# Conflicts:
#	submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift
This commit is contained in:
Ali 2022-02-22 22:21:20 +04:00
commit b8d96fa0af
99 changed files with 6497 additions and 1485 deletions

Binary file not shown.

View file

@ -7268,9 +7268,40 @@ Sorry for the inconvenience.";
"Attachment.SelectFromGallery" = "Select from Gallery";
"Attachment.SelectFromFiles" = "Select from Files";
"Attachment.AllMedia" = "All";
"Attachment.SelectedMedia_1" = "%@ Selected";
"Attachment.SelectedMedia_2" = "%@ Selected";
"Attachment.SelectedMedia_3_10" = "%@ Selected";
"Attachment.SelectedMedia_any" = "%@ Selected";
"Attachment.SelectedMedia_many" = "%@ Selected";
"Attachment.SelectedMedia_0" = "%@ Selected";
"Attachment.SendAsFile" = "Send as File";
"Attachment.SendAsFiles" = "Send as Files";
"Attachment.Grouped" = "Grouped";
"Attachment.Ungrouped" = "Ungrouped";
"Attachment.MessagePreview" = "Message Preview";
"Attachment.DragToReorder" = "Drag media to reorder";
"Attachment.SearchWeb" = "Search Web";
"Attachment.RecentlySentFiles" = "Recently Sent Files";
"ReportPeer.ReasonIllegalDrugs" = "Illegal Drugs";
"ReportPeer.ReasonPersonalDetails" = "Personal Details";
"Attachment.FilesIntro" = "Send and receive files of any type, up to 2 GB\nin size each, access them instantly\non your other devices.";
"Attachment.FilesSearchPlaceholder" = "Search sent files";
"Attachment.MediaAccessTitle" = "Access Your Photos and Videos";
"Attachment.MediaAccessText" = "Share an unlimited number of photos and videos of up to 2 GB each.";
"Attachment.LimitedMediaAccessText" = "You have limited Telegram from accessing all of your photos.";
"Attachment.CameraAccessText" = "Telegram needs camera access so that you can take photos and videos.";
"Attachment.Manage" = "Manage";
"Attachment.OpenSettings" = "Go to Settings";
"Attachment.OpenCamera" = "Open Camera";

View file

@ -3,7 +3,7 @@ import Display
import SwiftSignalKit
public protocol ContactSelectionController: ViewController {
var result: Signal<([ContactListPeer], ContactListAction)?, NoError> { get }
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?)?, NoError> { get }
var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { get set }

View file

@ -15,8 +15,8 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
let sendButton: HighlightTrackingButtonNode
var sendButtonHasApplyIcon = false
var animatingSendButton = false
let expandMediaInputButton: HighlightableButtonNode
let textNode: ImmediateTextNode
var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)?
private var gestureRecognizer: ContextGesture?
@ -40,8 +40,10 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
self.backgroundNode.clipsToBounds = true
self.sendButton = HighlightTrackingButtonNode(pointerStyle: .lift)
self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .default)
self.textNode = ImmediateTextNode()
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
self.textNode.isUserInteractionEnabled = false
super.init()
@ -71,7 +73,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
self.addSubnode(self.sendContainerNode)
self.sendContainerNode.addSubnode(self.backgroundNode)
self.sendContainerNode.addSubnode(self.sendButton)
self.addSubnode(self.expandMediaInputButton)
self.sendContainerNode.addSubnode(self.textNode)
}
override func didLoad() {
@ -91,9 +93,9 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
}
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
self.expandMediaInputButton.setImage(PresentationResourcesChat.chatInputPanelExpandButtonImage(theme), for: [])
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
}
private var absoluteRect: (CGRect, CGSize)?
@ -101,22 +103,31 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
self.absoluteRect = (rect, containerSize)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, interfaceState: ChatPresentationInterfaceState) -> CGSize {
self.validLayout = size
transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.sendContainerNode, frame: CGRect(origin: CGPoint(), size: size))
let backgroundSize = CGSize(width: 33.0, height: 33.0)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize))
self.backgroundNode.cornerRadius = backgroundSize.width / 2.0
transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size))
var expanded = false
if case let .media(_, maybeExpanded, _) = interfaceState.inputMode, maybeExpanded != nil {
expanded = true
let width: CGFloat
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: size.height))
if minimized {
width = 44.0
} else {
width = textSize.width + 36.0
}
transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0))
let buttonSize = CGSize(width: width, height: size.height)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
transition.updateAlpha(node: self.textNode, alpha: minimized ? 0.0 : 1.0)
transition.updateAlpha(node: self.sendButton.imageNode, alpha: minimized ? 1.0 : 0.0)
transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: buttonSize))
transition.updateFrame(node: self.sendContainerNode, frame: CGRect(origin: CGPoint(), size: buttonSize))
let backgroundSize = CGSize(width: width - 11.0, height: 33.0)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize))
self.backgroundNode.cornerRadius = backgroundSize.height / 2.0
return buttonSize
}
func updateAccessibility() {

View file

@ -287,7 +287,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
self.actionButtons.sendButton.alpha = 1.0
self.actionButtons.expandMediaInputButton.alpha = 0.0
self.actionButtons.updateAccessibility()
self.addSubnode(self.textInputContainer)
@ -590,9 +589,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: [])
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme), for: [])
} else {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: [])
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme), for: [])
}
}
}
@ -608,9 +607,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics)
var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
var composeButtonsOffset: CGFloat = 0.0
let textInputBackgroundWidthOffset: CGFloat = 0.0
self.updateCounterTextNode(transition: transition)
var inputHasText = false
@ -629,7 +625,15 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
if self.isCaption {
if !self.isFocused {
if self.isFocused {
self.oneLineNode.alpha = 0.0
self.oneLineDustNode?.alpha = 0.0
self.textInputNode?.alpha = 1.0
transition.updateAlpha(node: self.actionButtons, alpha: 1.0)
transition.updateTransformScale(node: self.actionButtons, scale: 1.0)
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: 1.0)
} else {
panelHeight = minimalHeight
transition.updateAlpha(node: self.oneLineNode, alpha: inputHasText ? 1.0 : 0.0)
@ -639,12 +643,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
if let textInputNode = self.textInputNode {
transition.updateAlpha(node: textInputNode, alpha: inputHasText ? 0.0 : 1.0)
}
} else {
self.oneLineNode.alpha = 0.0
self.oneLineDustNode?.alpha = 0.0
self.textInputNode?.alpha = 1.0
transition.updateAlpha(node: self.actionButtons, alpha: 0.0)
transition.updateTransformScale(node: self.actionButtons, scale: 0.001)
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: inputHasText ? 1.0 : 0.0)
}
let oneLineSize = self.oneLineNode.updateLayout(CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: CGFloat.greatestFiniteMagnitude))
let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineSize)
self.oneLineNode.frame = oneLineFrame
@ -652,34 +656,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.updateOneLineSpoiler()
}
self.textPlaceholderNode.isHidden = inputHasText
if self.isCaption {
if self.isFocused {
transition.updateAlpha(node: self.actionButtons, alpha: 1.0)
transition.updateTransformScale(node: self.actionButtons, scale: 1.0)
composeButtonsOffset = 0.0
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: 1.0)
} else {
transition.updateAlpha(node: self.actionButtons, alpha: 0.0)
transition.updateTransformScale(node: self.actionButtons, scale: 0.001)
composeButtonsOffset = 36.0
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: inputHasText ? 1.0 : 0.0)
}
}
let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight))
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
if let presentationInterfaceState = self.presentationInterfaceState {
self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState)
}
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
let textInputBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: textInputFrame.size.width + composeButtonsOffset, height: textInputFrame.size.height))
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: textInputBackgroundFrame)
if let textInputNode = self.textInputNode {
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
@ -690,20 +669,72 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: transition)
self.actionButtons.updateAccessibility()
return panelHeight
}
private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, transition: ContainedViewLayoutTransition) {
guard let (width, leftInset, rightInset, additionalSideInsets, _, metrics, _) = self.validLayout else {
return
}
var textFieldMinHeight: CGFloat = 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
}
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
var panelHeight = panelHeight
var composeButtonsOffset: CGFloat = 0.0
if self.isCaption {
if self.isFocused {
composeButtonsOffset = 0.0
} else {
composeButtonsOffset = 36.0
panelHeight = minimalHeight
}
}
let baseWidth = width - leftInset - rightInset
let textInputFrame = self.textInputContainer.frame
var textBackgroundInset: CGFloat = 0.0
let actionButtonsSize: CGSize
if let presentationInterfaceState = self.presentationInterfaceState {
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: !self.isAttachment || inputHasText, interfaceState: presentationInterfaceState)
textBackgroundInset = 44.0 - actionButtonsSize.width
} else {
actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)
}
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if additionalSideInsets.right > 0.0 {
textFieldInsets.right += additionalSideInsets.right / 3.0
}
let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - actionButtonsSize.width + 1.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: actionButtonsSize)
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
let textInputBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: textInputFrame.size.width + composeButtonsOffset + textBackgroundInset, height: textInputFrame.size.height))
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: textInputBackgroundFrame)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset + textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset + textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState)
}
let placeholderFrame: CGRect
if self.isCaption && !self.isFocused {
placeholderFrame = CGRect(origin: CGPoint(x: textInputFrame.minX + floorToScreenPixels((textInputBackgroundFrame.width - self.textPlaceholderNode.frame.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
} else {
placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
}
transition.updateFrame(node: self.textPlaceholderNode, frame: placeholderFrame)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + composeButtonsOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + composeButtonsOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
self.actionButtons.updateAccessibility()
return panelHeight
}
private var skipUpdate = false
@ -952,7 +983,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.oneLineNode.attributedText = nil
}
self.updateTextHeight(animated: animated)
let panelHeight = self.updateTextHeight(animated: animated)
if self.isAttachment, let panelHeight = panelHeight {
self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
private func updateOneLineSpoiler() {
@ -977,7 +1011,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
private func updateTextHeight(animated: Bool) {
private func updateTextHeight(animated: Bool) -> CGFloat? {
if let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, _) = self.validLayout {
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right, maxHeight: maxHeight, metrics: metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
@ -985,6 +1019,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.updateHeight(animated)
self.heightUpdated?(animated)
}
return panelHeight
} else {
return nil
}
}
@ -1002,12 +1039,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: [])
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme), for: [])
} else {
if case .scheduledMessages = interfaceState.subject {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme), for: [])
} else {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: [])
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme), for: [])
}
}
}

View file

@ -6,6 +6,7 @@ import UIKitRuntimeUtils
import Display
import DirectionalPanGesture
import TelegramPresentationData
import MapKit
final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let wrappingNode: ASDisplayNode
@ -44,10 +45,9 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
private var panGestureRecognizer: UIPanGestureRecognizer?
init(presentationData: PresentationData) {
override init() {
self.wrappingNode = ASDisplayNode()
self.clipNode = ASDisplayNode()
self.clipNode.backgroundColor = presentationData.theme.list.plainBackgroundColor
self.container = NavigationContainer(controllerRemoved: { _ in })
self.container.clipsToBounds = true
@ -87,6 +87,9 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
if let _ = otherGestureRecognizer.view?.superview as? MKMapView {
return false
}
return true
}
return false
@ -94,7 +97,17 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
let defaultTopInset: CGFloat = 210.0
private var defaultTopInset: CGFloat {
guard let (layout, _, _) = self.validLayout else{
return 210.0
}
if case .compact = layout.metrics.widthClass {
return max(layout.size.width, layout.size.height) * 0.2488
} else {
return 210.0
}
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard let (layout, controllers, coveredByModalTransition) = self.validLayout else {
return
@ -107,6 +120,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
case .began:
let point = recognizer.location(in: self.view)
let currentHitView = self.hitTest(point, with: nil)
let scrollViewAndListNode = self.findScrollView(view: currentHitView)
let scrollView = scrollViewAndListNode?.0
let listNode = scrollViewAndListNode?.1
@ -174,12 +188,14 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let translation = recognizer.translation(in: self.view).y
var velocity = recognizer.velocity(in: self.view)
if case let .known(value) = visibleContentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = visibleContentOffset {
velocity = CGPoint()
} else if contentOffset > 0.0 {
velocity = CGPoint()
if self.isExpanded {
if case let .known(value) = visibleContentOffset, value > 0.1 {
velocity = CGPoint()
} else if case .unknown = visibleContentOffset {
velocity = CGPoint()
} else if contentOffset > 0.1 {
velocity = CGPoint()
}
}
var bounds = self.bounds
@ -217,7 +233,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.interactivelyDismissed?()
dismissing = true
} else if (velocity.y < -300.0 || offset < topInset / 2.0) {
if velocity.y > -2200.0, let listNode = listNode {
if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode {
DispatchQueue.main.async {
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
@ -286,7 +302,6 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.validLayout = (layout, controllers, coveredByModalTransition)
self.panGestureRecognizer?.isEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0)
// self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled
let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
@ -303,7 +318,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
transition.updateFrame(node: self.wrappingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size))
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
self.updateModalProgress?(modalProgress, transition)
let containerLayout: ContainerViewLayout
@ -344,24 +359,24 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let effectiveStatusBarHeight: CGFloat? = nil
let inset: CGFloat = 70.0
let overflowInset: CGFloat = 70.0
var safeInsets = layout.safeInsets
safeInsets.left += inset
safeInsets.right += inset
safeInsets.left += overflowInset
safeInsets.right += overflowInset
var intrinsicInsets = layout.intrinsicInsets
intrinsicInsets.left += inset
intrinsicInsets.right += inset
intrinsicInsets.left += overflowInset
intrinsicInsets.right += overflowInset
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width + inset * 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 + 49.0, 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: 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)
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
let maxScaledTopInset: CGFloat = containerTopInset - 10.0
let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
containerFrame = unscaledFrame.offsetBy(dx: -inset, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
containerFrame = unscaledFrame.offsetBy(dx: -overflowInset, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
clipFrame = CGRect(x: containerFrame.minX + inset, y: containerFrame.minY, width: containerFrame.width - inset * 2.0, height: containerFrame.height)
clipFrame = CGRect(x: containerFrame.minX + overflowInset, y: containerFrame.minY, width: containerFrame.width - overflowInset * 2.0, height: containerFrame.height)
}
} else {
self.clipNode.clipsToBounds = true
@ -451,61 +466,10 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if !self.wrappingNode.frame.contains(point) {
let convertedPoint = self.view.convert(point, to: self.container.view)
if !self.container.frame.contains(convertedPoint) {
return false
}
return super.point(inside: point, with: event)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
// var currentParent: UIView? = result
// var enableScrolling = true
// while true {
// if currentParent == nil {
// break
// }
// if currentParent is UIKeyInput {
// if currentParent?.disablesInteractiveModalDismiss == true {
// enableScrolling = false
// break
// }
// } else if let scrollView = currentParent as? UIScrollView {
// if scrollView === self.scrollNode.view {
// break
// }
// if scrollView.disablesInteractiveModalDismiss {
// enableScrolling = false
// break
// } else {
// if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
// return self.scrollNode.view
// }
// }
// } else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
// if listNode.view.disablesInteractiveModalDismiss {
// enableScrolling = false
// break
// } else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
// return self.scrollNode.view
// }
// }
// currentParent = currentParent?.superview
// }
// if let controller = self.container.controllers.last {
// if controller.view.disablesInteractiveModalDismiss {
// enableScrolling = false
// }
// }
// self.isInteractiveDimissEnabled = enableScrolling
// if let layout = self.validLayout {
// if layout.inputHeight != nil && layout.inputHeight != 0.0 {
// enableScrolling = false
// }
// }
// self.scrollNode.view.isScrollEnabled = enableScrolling
return result
}
}

View file

@ -12,7 +12,6 @@ import TelegramStringFormatting
import UIKitRuntimeUtils
public enum AttachmentButtonType: Equatable {
case camera
case gallery
case file
case location
@ -23,6 +22,19 @@ public enum AttachmentButtonType: Equatable {
public protocol AttachmentContainable: ViewController {
var requestAttachmentMenuExpansion: () -> Void { get set }
func prepareForReuse()
}
public extension AttachmentContainable {
func prepareForReuse() {
}
}
public enum AttachmentMediaPickerSendMode {
case media
case files
}
public protocol AttachmentMediaPickerContext {
@ -30,13 +42,23 @@ public protocol AttachmentMediaPickerContext {
var caption: Signal<NSAttributedString?, NoError> { get }
func setCaption(_ caption: NSAttributedString)
func send(silently: Bool)
func send(silently: Bool, mode: AttachmentMediaPickerSendMode)
func schedule()
}
public class AttachmentController: ViewController {
private let context: AccountContext
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private let buttons: [AttachmentButtonType]
public var mediaPickerContext: AttachmentMediaPickerContext? {
get {
return self.node.mediaPickerContext
}
set {
self.node.mediaPickerContext = newValue
}
}
private final class Node: ASDisplayNode {
private weak var controller: AttachmentController?
@ -44,16 +66,19 @@ public class AttachmentController: ViewController {
private let container: AttachmentContainer
let panel: AttachmentPanel
private var validLayout: ContainerViewLayout?
private var modalProgress: CGFloat = 0.0
private var currentType: AttachmentButtonType?
private var currentController: AttachmentContainable?
private var validLayout: ContainerViewLayout?
private var modalProgress: CGFloat = 0.0
private var isDismissing = false
private let captionDisposable = MetaDisposable()
private let mediaSelectionCountDisposable = MetaDisposable()
private var mediaPickerContext: AttachmentMediaPickerContext? {
private var selectionCount: Int = 0
fileprivate var mediaPickerContext: AttachmentMediaPickerContext? {
didSet {
if let mediaPickerContext = self.mediaPickerContext {
self.captionDisposable.set((mediaPickerContext.caption
@ -82,17 +107,16 @@ public class AttachmentController: ViewController {
self.dim.alpha = 0.0
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
self.container = AttachmentContainer(presentationData: presentationData)
self.container = AttachmentContainer()
self.container.canHaveKeyboardFocus = true
self.panel = AttachmentPanel(context: controller.context)
self.panel = AttachmentPanel(context: controller.context, updatedPresentationData: controller.updatedPresentationData)
super.init()
self.addSubnode(self.dim)
self.container.updateModalProgress = { [weak self] progress, transition in
if let strongSelf = self, let layout = strongSelf.validLayout {
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
strongSelf.modalProgress = progress
@ -133,9 +157,9 @@ public class AttachmentController: ViewController {
if let strongSelf = self {
switch mode {
case .generic:
strongSelf.mediaPickerContext?.send(silently: false)
strongSelf.mediaPickerContext?.send(silently: false, mode: .media)
case .silent:
strongSelf.mediaPickerContext?.send(silently: true)
strongSelf.mediaPickerContext?.send(silently: true, mode: .media)
case .schedule:
strongSelf.mediaPickerContext?.schedule()
}
@ -174,7 +198,6 @@ public class AttachmentController: ViewController {
self.switchToController(.gallery, false)
}
private var selectionCount: Int = 0
private func updateSelectionCount(_ count: Int) {
self.selectionCount = count
if let layout = self.validLayout {
@ -188,31 +211,11 @@ public class AttachmentController: ViewController {
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let controller = self.controller, controller.isInteractionDisabled() {
return self.view
} else {
return super.hitTest(point, with: event)
}
}
func dismiss(animated: Bool, completion: @escaping () -> Void = {}) {
if animated {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
})
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition)
} else {
self.controller?.dismiss(animated: false, completion: nil)
}
}
func switchToController(_ type: AttachmentButtonType, _ ascending: Bool) {
guard self.currentType != type else {
if let controller = self.currentController {
controller.scrollToTopWithTabBar?()
}
return
}
let previousType = self.currentType
@ -227,6 +230,7 @@ public class AttachmentController: ViewController {
self?.container.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
}
let previousController = strongSelf.currentController
let animateTransition = previousType != nil
strongSelf.currentController = controller
@ -243,15 +247,10 @@ public class AttachmentController: ViewController {
guard let strongSelf = self else {
return
}
if ascending {
strongSelf.container.container.view.layer.animatePosition(from: CGPoint(x: 70.0, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
} else {
strongSelf.container.container.view.layer.animatePosition(from: CGPoint(x: -70.0, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
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?.prepareForReuse()
})
})
}
@ -266,10 +265,42 @@ public class AttachmentController: ViewController {
})
}
func animateIn(transition: ContainedViewLayoutTransition) {
func animateIn() {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
let targetPosition = self.container.position
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height)
self.container.position = startPosition
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
transition.animateView(allowUserInteraction: true, {
self.container.position = targetPosition
})
}
func animateOut(completion: @escaping () -> Void = {}) {
self.isDismissing = true
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0), completion: { [weak self] _ in
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
})
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition)
}
func scrollToTop() {
self.currentController?.scrollToTop?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let controller = self.controller, controller.isInteractionDisabled() {
return self.view
} else {
return super.hitTest(point, with: event)
}
}
private var isCollapsed: Bool = false
@ -279,31 +310,7 @@ public class AttachmentController: ViewController {
self.validLayout = layout
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
let containerTransition: ContainedViewLayoutTransition
if self.container.supernode == nil {
containerTransition = .immediate
} else {
containerTransition = transition
}
if !self.isUpdatingContainer {
self.isUpdatingContainer = true
let controllers = self.currentController.flatMap { [$0] } ?? []
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: layout.size))
self.container.update(layout: layout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
self.addSubnode(self.container)
self.container.addSubnode(self.panel)
self.animateIn(transition: transition)
}
self.isUpdatingContainer = false
}
if self.modalProgress < 0.5 {
self.isCollapsed = false
} else if self.modalProgress == 1.0 {
@ -317,6 +324,35 @@ public class AttachmentController: ViewController {
panelTransition = .animated(duration: 0.25, curve: .easeInOut)
}
panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)))
if !self.isUpdatingContainer && !self.isDismissing {
self.isUpdatingContainer = true
let containerTransition: ContainedViewLayoutTransition
if self.container.supernode == nil {
containerTransition = .immediate
} else {
containerTransition = transition
}
let controllers = self.currentController.flatMap { [$0] } ?? []
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: layout.size))
var containerInsets = layout.intrinsicInsets
containerInsets.bottom = panelHeight
let containerLayout = layout.withUpdatedIntrinsicInsets(containerInsets)
self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
self.addSubnode(self.container)
self.container.addSubnode(self.panel)
self.animateIn()
}
self.isUpdatingContainer = false
}
}
}
@ -324,15 +360,22 @@ public class AttachmentController: ViewController {
completion(nil, nil)
}
public init(context: AccountContext, buttons: [AttachmentButtonType]) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, buttons: [AttachmentButtonType]) {
self.context = context
self.buttons = buttons
self.updatedPresentationData = updatedPresentationData
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.node.scrollToTop()
}
}
}
public required init(coder aDecoder: NSCoder) {
@ -351,8 +394,8 @@ public class AttachmentController: ViewController {
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.view.endEditing(true)
if flag {
self.node.dismiss(animated: true, completion: {
super.dismiss(animated: flag, completion: {})
self.node.animateOut(completion: {
super.dismiss(animated: false, completion: {})
completion?()
})
} else {

View file

@ -3,6 +3,7 @@ import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
@ -12,7 +13,7 @@ import ChatPresentationInterfaceState
import ChatSendMessageActionUI
import ChatTextLinkEditUI
private let buttonSize = CGSize(width: 75.0, height: 49.0)
private let buttonSize = CGSize(width: 88.0, height: 49.0)
private let iconSize = CGSize(width: 30.0, height: 30.0)
private let sideInset: CGFloat = 0.0
@ -26,8 +27,6 @@ private final class AttachButtonComponent: CombinedComponent {
let context: AccountContext
let type: AttachmentButtonType
let isSelected: Bool
let isCollapsed: Bool
let transitionFraction: CGFloat
let strings: PresentationStrings
let theme: PresentationTheme
let action: () -> Void
@ -36,8 +35,6 @@ private final class AttachButtonComponent: CombinedComponent {
context: AccountContext,
type: AttachmentButtonType,
isSelected: Bool,
isCollapsed: Bool,
transitionFraction: CGFloat,
strings: PresentationStrings,
theme: PresentationTheme,
action: @escaping () -> Void
@ -45,8 +42,6 @@ private final class AttachButtonComponent: CombinedComponent {
self.context = context
self.type = type
self.isSelected = isSelected
self.isCollapsed = isCollapsed
self.transitionFraction = transitionFraction
self.strings = strings
self.theme = theme
self.action = action
@ -62,12 +57,6 @@ private final class AttachButtonComponent: CombinedComponent {
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.isCollapsed != rhs.isCollapsed {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -80,43 +69,33 @@ private final class AttachButtonComponent: CombinedComponent {
static var body: Body {
let icon = Child(Image.self)
let title = Child(Text.self)
let button = Child(Rectangle.self)
return { context in
let name: String
let animationName: String?
let imageName: String?
let component = context.component
let strings = component.strings
switch component.type {
case .camera:
name = strings.Attachment_Camera
animationName = "anim_camera"
imageName = "Chat/Attach Menu/Camera"
case .gallery:
name = strings.Attachment_Gallery
animationName = "anim_gallery"
imageName = "Chat/Attach Menu/Gallery"
case .file:
name = strings.Attachment_File
animationName = "anim_file"
imageName = "Chat/Attach Menu/File"
case .location:
name = strings.Attachment_Location
animationName = "anim_location"
imageName = "Chat/Attach Menu/Location"
case .contact:
name = strings.Attachment_Contact
animationName = "anim_contact"
imageName = "Chat/Attach Menu/Contact"
case .poll:
name = strings.Attachment_Poll
animationName = "anim_poll"
imageName = "Chat/Attach Menu/Poll"
case let .app(appName):
name = appName
animationName = nil
imageName = nil
}
@ -124,13 +103,14 @@ private final class AttachButtonComponent: CombinedComponent {
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
let icon = icon.update(
component: Image(image: image, tintColor: tintColor),
component: Image(
image: image,
tintColor: tintColor
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: context.transition
)
print(animationName ?? "")
let title = title.update(
component: Text(
text: name,
@ -140,8 +120,18 @@ private final class AttachButtonComponent: CombinedComponent {
availableSize: context.availableSize,
transition: .immediate
)
let button = button.update(
component: Rectangle(
color: .clear,
width: context.availableSize.width,
height: context.availableSize.height
),
availableSize: context.availableSize,
transition: .immediate
)
let topInset: CGFloat = 5.0 + UIScreenPixel
let topInset: CGFloat = 4.0 + UIScreenPixel
let spacing: CGFloat = 15.0 + UIScreenPixel
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - icon.size.width) / 2.0), y: topInset), size: icon.size)
@ -149,18 +139,19 @@ private final class AttachButtonComponent: CombinedComponent {
context.add(title
.position(CGPoint(x: titleFrame.midX, y: titleFrame.midY))
.gesture(.tap {
component.action()
})
)
context.add(icon
.position(CGPoint(x: iconFrame.midX, y: iconFrame.midY))
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
.gesture(.tap {
component.action()
})
)
return context.availableSize
}
}
@ -169,21 +160,21 @@ private final class AttachButtonComponent: CombinedComponent {
final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
private let containerNode: ASDisplayNode
private var effectView: UIVisualEffectView?
private let backgroundNode: NavigationBackgroundNode
private let scrollNode: ASScrollNode
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private var buttonViews: [Int: ComponentHostView<Empty>] = [:]
private var textInputPanelNode: AttachmentTextInputPanelNode?
private var buttons: [AttachmentButtonType] = []
private var selectedIndex: Int = 1
private var selectedIndex: Int = 0
private(set) var isCollapsed: Bool = false
private(set) var isSelecting: Bool = false
@ -198,9 +189,9 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
var present: (ViewController) -> Void = { _ in }
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
init(context: AccountContext) {
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
@ -208,11 +199,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
self.containerNode.clipsToBounds = true
self.scrollNode = ASScrollNode()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
super.init()
@ -362,7 +352,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
guard let textInputNode = textInputPanelNode.textInputNode else {
return
}
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, completion: {
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, attachment: true, completion: {
}, sendMessage: { [weak textInputPanelNode] silently in
textInputPanelNode?.sendMessage(silently ? .silent : .generic)
}, schedule: { [weak textInputPanelNode] in
@ -387,6 +377,26 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}, presentChatRequestAdminInfo: {
}, displayCopyProtectionTip: { _, _ in
}, statuses: nil)
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
strongSelf.backgroundNode.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
strongSelf.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) })
if let layout = strongSelf.validLayout {
let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isCollapsed: strongSelf.isCollapsed, isSelecting: strongSelf.isSelecting, transition: .immediate)
}
}
})
}
deinit {
self.presentationDataDisposable?.dispose()
}
override func didLoad() {
@ -398,17 +408,6 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.view.delegate = self
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
let effect: UIVisualEffect
switch self.presentationData.theme.actionSheet.backgroundType {
case .light:
effect = UIBlurEffect(style: .light)
case .dark:
effect = UIBlurEffect(style: .dark)
}
let effectView = UIVisualEffectView(effect: effect)
self.effectView = effectView
self.containerNode.view.insertSubview(effectView, at: 0)
}
func updateCaption(_ caption: NSAttributedString) {
@ -441,33 +440,26 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}
let visibleRect = self.scrollNode.bounds.insetBy(dx: -180.0, dy: 0.0)
let actualVisibleRect = self.scrollNode.bounds
var validButtons = Set<Int>()
var sideInset = sideInset
let buttonsWidth = sideInset * 2.0 + buttonSize.width * CGFloat(self.buttons.count)
if buttonsWidth < layout.size.width {
sideInset = floorToScreenPixels((layout.size.width - buttonsWidth) / 2.0)
}
let distanceBetweenNodes = layout.size.width / CGFloat(self.buttons.count)
let internalWidth = distanceBetweenNodes * CGFloat(self.buttons.count - 1)
let leftNodeOriginX = (layout.size.width - internalWidth) / 2.0
// var sideInset = sideInset
// let buttonsWidth = sideInset * 2.0 + buttonSize.width * CGFloat(self.buttons.count)
// if buttonsWidth < layout.size.width {
// sideInset = floorToScreenPixels((layout.size.width - buttonsWidth) / 2.0)
// }
//
for i in 0 ..< self.buttons.count {
let buttonFrame = CGRect(origin: CGPoint(x: sideInset + buttonSize.width * CGFloat(i), y: 0.0), size: buttonSize)
let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - buttonSize.width / 2.0)
let buttonFrame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: buttonSize)
if !visibleRect.intersects(buttonFrame) {
continue
}
validButtons.insert(i)
let edge = buttonSize.width * 0.75
let leftEdge = max(-edge, min(0.0, buttonFrame.minX - actualVisibleRect.minX)) / -edge
let rightEdge = min(edge, max(0.0, buttonFrame.maxX - actualVisibleRect.maxX)) / edge
let transitionFraction: CGFloat
if leftEdge > rightEdge {
transitionFraction = leftEdge
} else {
transitionFraction = -rightEdge
}
var buttonTransition = transition
let buttonView: ComponentHostView<Empty>
if let current = self.buttonViews[i] {
@ -486,8 +478,6 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
context: self.context,
type: type,
isSelected: i == self.selectedIndex,
isCollapsed: self.isCollapsed,
transitionFraction: transitionFraction,
strings: self.presentationData.strings,
theme: self.presentationData.theme,
action: { [weak self] in
@ -514,13 +504,14 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
return false
}
var sideInset = sideInset
let buttonsWidth = sideInset * 2.0 + buttonSize.width * CGFloat(self.buttons.count)
if buttonsWidth < layout.size.width {
sideInset = floorToScreenPixels((layout.size.width - buttonsWidth) / 2.0)
}
// var sideInset = sideInset
// let buttonsWidth = sideInset * 2.0 + buttonSize.width * CGFloat(self.buttons.count)
// if buttonsWidth < layout.size.width {
// sideInset = floorToScreenPixels((layout.size.width - buttonsWidth) / 2.0)
// }
let contentSize = CGSize(width: sideInset * 2.0 + CGFloat(self.buttons.count) * buttonSize.width, height: buttonSize.height)
let contentSize = CGSize(width: layout.size.width, height: buttonSize.height)
// CGSize(width: sideInset * 2.0 + CGFloat(self.buttons.count) * buttonSize.width, height: buttonSize.height)
self.scrollLayout = (layout.size.width, contentSize)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSelecting ? -buttonSize.height : 0.0), size: CGSize(width: layout.size.width, height: buttonSize.height)))
@ -593,7 +584,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if textInputPanelNode.frame.width.isZero {
panelTransition = .immediate
}
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left, rightInset: insets.right, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics)
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics)
let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight)
if textInputPanelNode.frame.width.isZero {
textInputPanelNode.frame = panelFrame
@ -642,10 +633,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
containerTransition.updateFrame(node: self.containerNode, frame: containerFrame)
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
self.backgroundNode.update(size: containerBounds.size, transition: transition)
containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel)))
if let effectView = self.effectView {
containerTransition.updateFrame(view: effectView, frame: bounds)
}
let _ = self.updateScrollLayoutIfNeeded(force: isCollapsedUpdated || isSelectingUpdated, transition: containerTransition)

View file

@ -385,7 +385,7 @@ public final class CallListController: TelegramBaseController {
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller, weak self] result in
controller?.dismissSearch()
if let strongSelf = self, let (contactPeers, action) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer {
if let strongSelf = self, let (contactPeers, action, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer {
strongSelf.call(peer.id, isVideo: action == .videoCall, began: {
if let strongSelf = self {
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()

View file

@ -19,6 +19,7 @@ public final class ChatSendMessageActionSheetController: ViewController {
private let gesture: ContextGesture
private let sourceSendButton: ASDisplayNode
private let textInputNode: EditableTextNode
private let attachment: Bool
private let completion: () -> Void
private let sendMessage: (Bool) -> Void
private let schedule: () -> Void
@ -32,12 +33,13 @@ public final class ChatSendMessageActionSheetController: ViewController {
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, completion: @escaping () -> Void, sendMessage: @escaping (Bool) -> Void, schedule: @escaping () -> Void) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool = false, completion: @escaping () -> Void, sendMessage: @escaping (Bool) -> Void, schedule: @escaping () -> Void) {
self.context = context
self.interfaceState = interfaceState
self.gesture = gesture
self.sourceSendButton = sourceSendButton
self.textInputNode = textInputNode
self.attachment = attachment
self.completion = completion
self.sendMessage = sendMessage
self.schedule = schedule
@ -69,8 +71,8 @@ public final class ChatSendMessageActionSheetController: ViewController {
}
override public func loadDisplayNode() {
var forwardedCount = 0
if let forwardMessageIds = self.interfaceState.interfaceState.forwardMessageIds {
var forwardedCount: Int?
if let forwardMessageIds = self.interfaceState.interfaceState.forwardMessageIds, forwardMessageIds.count > 0 {
forwardedCount = forwardMessageIds.count
}
@ -83,7 +85,7 @@ public final class ChatSendMessageActionSheetController: ViewController {
canSchedule = !isSecret
}
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, attachment: self.attachment, forwardedCount: forwardedCount, send: { [weak self] in
self?.sendMessage(false)
self?.dismiss(cancel: false)
}, sendSilently: { [weak self] in

View file

@ -156,6 +156,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
private let sourceSendButton: ASDisplayNode
private let textFieldFrame: CGRect
private let textInputNode: EditableTextNode
private let attachment: Bool
private let forwardedCount: Int?
private let send: (() -> Void)?
@ -180,12 +181,15 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
return self.sourceSendButton.view.convert(self.sourceSendButton.bounds, to: nil)
}
init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) {
private var animateInputField = false
init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) {
self.context = context
self.presentationData = presentationData
self.sourceSendButton = sourceSendButton
self.textFieldFrame = textInputNode.convert(textInputNode.bounds, to: nil)
self.textInputNode = textInputNode
self.attachment = attachment
self.forwardedCount = forwardedCount
self.send = send
@ -245,10 +249,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
super.init()
// self.sendButtonNode.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.presentationData.theme), for: [])
self.sendButtonNode.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
if let attributedText = textInputNode.attributedText, !attributedText.string.isEmpty {
self.animateInputField = true
self.fromMessageTextNode.attributedText = attributedText
if let toAttributedText = self.fromMessageTextNode.attributedText?.mutableCopy() as? NSMutableAttributedString {
@ -256,7 +260,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
self.toMessageTextNode.attributedText = toAttributedText
}
} else {
self.fromMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_InputTextPlaceholder, attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.inputPanel.inputPlaceholderColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)])
if let _ = forwardedCount {
self.animateInputField = true
}
self.fromMessageTextNode.attributedText = NSAttributedString(string: self.attachment ? self.presentationData.strings.MediaPicker_AddCaption : self.presentationData.strings.Conversation_InputTextPlaceholder, attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.inputPanel.inputPlaceholderColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)])
self.toMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ForwardedMessages(Int32(forwardedCount ?? 0)), attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.message.outgoing.primaryTextColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)])
}
@ -359,7 +366,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor
self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor
// self.sendButtonNode.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.presentationData.theme), for: [])
if let toAttributedText = self.textInputNode.attributedText?.mutableCopy() as? NSMutableAttributedString {
toAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: self.presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (toAttributedText.string as NSString).length))
@ -376,6 +382,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
}
func animateIn() {
guard let layout = self.validLayout else {
return
}
self.textInputNode.textView.setContentOffset(self.textInputNode.textView.contentOffset, animated: false)
UIView.animate(withDuration: 0.2, animations: {
@ -389,61 +399,66 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.messageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.fromMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.toMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false)
self.textInputNode.isHidden = true
self.sourceSendButton.isHidden = true
if let layout = self.validLayout {
let duration = 0.4
self.sendButtonNode.layer.animateScale(from: 0.75, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
self.sendButtonNode.layer.animatePosition(from: self.sendButtonFrame.center, to: self.sendButtonNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
var initialWidth = self.textFieldFrame.width + 32.0
if self.textInputNode.textView.attributedText.string.isEmpty {
initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0)
}
let fromFrame = CGRect(origin: CGPoint(), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 2.0))
let delta = (fromFrame.height - self.messageClipNode.bounds.height) / 2.0
let inputHeight = layout.inputHeight ?? 0.0
var clipDelta = delta
if inputHeight.isZero || layout.isNonExclusive {
clipDelta -= self.contentContainerNode.frame.height + 16.0
}
self.messageClipNode.layer.animateBounds(from: fromFrame, to: self.messageClipNode.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
self.messageClipNode.layer.animatePosition(from: CGPoint(x: (self.messageClipNode.bounds.width - initialWidth) / 2.0, y: clipDelta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.insertSubnode(strongSelf.contentContainerNode, aboveSubnode: strongSelf.scrollNode)
}
})
self.messageBackgroundNode.layer.animateBounds(from: fromFrame, to: self.messageBackgroundNode.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
self.messageBackgroundNode.layer.animatePosition(from: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
var textXOffset: CGFloat = 0.0
let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height
if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL {
textXOffset = initialWidth - self.messageClipNode.bounds.width
}
self.fromMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.toMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY)
let springDuration: Double = 0.42
let springDamping: CGFloat = 104.0
self.contentContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
if self.animateInputField {
self.fromMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.toMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false)
self.textInputNode.isHidden = true
} else {
self.messageBackgroundNode.isHidden = true
self.fromMessageTextNode.isHidden = true
self.toMessageTextNode.isHidden = true
}
let duration = 0.4
self.sendButtonNode.layer.animateScale(from: 0.75, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
self.sendButtonNode.layer.animatePosition(from: self.sendButtonFrame.center, to: self.sendButtonNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
var initialWidth = self.textFieldFrame.width + 32.0
if self.textInputNode.textView.attributedText.string.isEmpty {
initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0)
}
let fromFrame = CGRect(origin: CGPoint(), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 2.0))
let delta = (fromFrame.height - self.messageClipNode.bounds.height) / 2.0
let inputHeight = layout.inputHeight ?? 0.0
var clipDelta = delta
if inputHeight.isZero || layout.isNonExclusive {
clipDelta -= self.contentContainerNode.frame.height + 16.0
}
self.messageClipNode.layer.animateBounds(from: fromFrame, to: self.messageClipNode.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
self.messageClipNode.layer.animatePosition(from: CGPoint(x: (self.messageClipNode.bounds.width - initialWidth) / 2.0, y: clipDelta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.insertSubnode(strongSelf.contentContainerNode, aboveSubnode: strongSelf.scrollNode)
}
})
self.messageBackgroundNode.layer.animateBounds(from: fromFrame, to: self.messageBackgroundNode.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
self.messageBackgroundNode.layer.animatePosition(from: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
var textXOffset: CGFloat = 0.0
let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height
if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL {
textXOffset = initialWidth - self.messageClipNode.bounds.width
}
self.fromMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.toMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY)
let springDuration: Double = 0.42
let springDamping: CGFloat = 104.0
self.contentContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
}
func animateOut(cancel: Bool, completion: @escaping () -> Void) {
guard let layout = self.validLayout else {
return
}
self.isUserInteractionEnabled = false
self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false)
@ -475,75 +490,77 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in })
if cancel {
self.fromMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false)
self.toMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false)
self.messageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false, completion: { _ in
completedAlpha = true
intermediateCompletion()
})
if self.animateInputField {
if cancel {
self.fromMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false)
self.toMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false)
self.messageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false, completion: { _ in
completedAlpha = true
intermediateCompletion()
})
} else {
self.textInputNode.isHidden = false
self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
completedAlpha = true
intermediateCompletion()
})
}
} else {
self.textInputNode.isHidden = false
self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
completedAlpha = true
intermediateCompletion()
})
completedAlpha = true
}
if let layout = self.validLayout {
let duration = 0.4
self.sendButtonNode.layer.animatePosition(from: self.sendButtonNode.position, to: self.sendButtonFrame.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completedButton = true
intermediateCompletion()
})
if !cancel {
self.sourceSendButton.isHidden = false
self.sendButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
self.sendButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
}
var initialWidth = self.textFieldFrame.width + 32.0
if self.textInputNode.textView.attributedText.string.isEmpty {
initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0)
}
let toFrame = CGRect(origin: CGPoint(), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 1.0))
let delta = (toFrame.height - self.messageClipNode.bounds.height) / 2.0
let duration = 0.4
self.sendButtonNode.layer.animatePosition(from: self.sendButtonNode.position, to: self.sendButtonFrame.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completedButton = true
intermediateCompletion()
})
if !cancel {
self.sourceSendButton.isHidden = false
self.sendButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
self.sendButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
}
var initialWidth = self.textFieldFrame.width + 32.0
if self.textInputNode.textView.attributedText.string.isEmpty {
initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0)
}
let toFrame = CGRect(origin: CGPoint(), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 1.0))
let delta = (toFrame.height - self.messageClipNode.bounds.height) / 2.0
if cancel && self.animateInputField {
let inputHeight = layout.inputHeight ?? 0.0
var clipDelta = delta
if inputHeight.isZero || layout.isNonExclusive {
clipDelta -= self.contentContainerNode.frame.height + 16.0
}
if cancel {
self.messageClipNode.layer.animateBounds(from: self.messageClipNode.bounds, to: toFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completedBubble = true
intermediateCompletion()
})
self.messageClipNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (self.messageClipNode.bounds.width - initialWidth) / 2.0, y: clipDelta + self.scrollNode.view.contentOffset.y), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.messageBackgroundNode.layer.animateBounds(from: self.messageBackgroundNode.bounds, to: toFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.messageBackgroundNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
var textXOffset: CGFloat = 0.0
let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height
if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL {
textXOffset = initialWidth - self.messageClipNode.bounds.width
}
self.fromMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.toMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
} else {
self.messageClipNode.layer.animateBounds(from: self.messageClipNode.bounds, to: toFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completedBubble = true
intermediateCompletion()
})
self.messageClipNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (self.messageClipNode.bounds.width - initialWidth) / 2.0, y: clipDelta + self.scrollNode.view.contentOffset.y), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.messageBackgroundNode.layer.animateBounds(from: self.messageBackgroundNode.bounds, to: toFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.messageBackgroundNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
var textXOffset: CGFloat = 0.0
let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height
if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL {
textXOffset = initialWidth - self.messageClipNode.bounds.width
}
let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY)
self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentOffset, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.fromMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.toMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
} else {
completedBubble = true
}
let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY)
self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentOffset, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -558,7 +575,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let sideInset: CGFloat = 43.0
let sideInset: CGFloat = self.sendButtonFrame.width - 1.0
var contentSize = CGSize()
contentSize.width = min(layout.size.width - 40.0, 250.0)
@ -578,7 +595,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
let contentOffset = self.scrollNode.view.contentOffset.y
var contentOrigin = CGPoint(x: layout.size.width - sideInset - contentSize.width - layout.safeInsets.right, y: layout.size.height - 6.0 - insets.bottom - contentSize.height)
if inputHeight > 0.0 && !layout.isNonExclusive {
if inputHeight > 0.0 && !layout.isNonExclusive && self.animateInputField {
contentOrigin.y += menuHeightWithInset
}
contentOrigin.y = min(contentOrigin.y + contentOffset, layout.size.height - 6.0 - layout.intrinsicInsets.bottom - contentSize.height)
@ -593,7 +610,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
let initialSendButtonFrame = self.sendButtonFrame
var sendButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - initialSendButtonFrame.width + 1.0 - UIScreenPixel - layout.safeInsets.right, y: layout.size.height - insets.bottom - initialSendButtonFrame.height), size: initialSendButtonFrame.size)
if inputHeight.isZero || layout.isNonExclusive {
if (inputHeight.isZero || layout.isNonExclusive) && self.animateInputField {
sendButtonFrame.origin.y -= menuHeightWithInset
}
sendButtonFrame.origin.y = min(sendButtonFrame.origin.y + contentOffset, layout.size.height - layout.intrinsicInsets.bottom - initialSendButtonFrame.height)

View file

@ -171,7 +171,15 @@ public class CheckNode: ASDisplayNode {
context.setLineWidth(borderWidth)
let maybeScaleOut = {
if parameters.animatingOut {
let animate: Bool
if case .counter = parameters.content {
animate = true
} else if parameters.animatingOut {
animate = true
} else {
animate = false
}
if animate {
context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0)
context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress)
context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0)
@ -206,35 +214,41 @@ public class CheckNode: ASDisplayNode {
let fillFrame = bounds.insetBy(dx: fillInset, dy: fillInset)
context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress)))
let scale = (bounds.width - inset) / 18.0
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (checkProgress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
switch parameters.content {
case .check:
let scale = (bounds.width - inset) / 18.0
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (checkProgress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
context.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.strokePath()
case let .counter(number):
let string = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .semibold), textColor: parameters.theme.strokeColor)
let stringSize = string.boundingRect(with: bounds.size, options: .usesLineFragmentOrigin, context: nil).size
string.draw(at: CGPoint(x: floorToScreenPixels((bounds.width - stringSize.width) / 2.0), y: floorToScreenPixels((bounds.height - stringSize.height) / 2.0)))
}
context.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.strokePath()
}
}
@ -270,11 +284,9 @@ public class InteractiveCheckNode: CheckNode {
return
}
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
transition.updateTransformScale(node: strongSelf, scale: 0.85)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
transition.updateTransformScale(node: strongSelf, scale: 1.0)
strongSelf.layer.animateScale(from: 1.0, to: 0.85, duration: 0.15, removeOnCompletion: false)
} else if let presentationLayer = strongSelf.layer.presentation() {
strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
}
}
@ -425,35 +437,40 @@ public class CheckLayer: CALayer {
let fillFrame = bounds.insetBy(dx: fillInset, dy: fillInset)
context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress)))
let scale = (bounds.width - inset) / 18.0
switch parameters.content {
case .check:
let scale = (bounds.width - inset) / 18.0
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (checkProgress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (checkProgress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
context.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.strokePath()
case let .counter(number):
let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor)
text.draw(at: CGPoint())
}
context.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.strokePath()
})?.cgImage
}
}

View file

@ -792,6 +792,12 @@ public func createPollController(context: AccountContext, updatedPresentationDat
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.Limits())
)
|> map { presentationData, state, limitsConfiguration -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
if presentationData.theme.list.blocksBackgroundColor.rgb == 0x000000 {
let updatedTheme = presentationData.theme.withInvertedBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
}
var enabled = true
if processPollText(state.text).isEmpty {
enabled = false

View file

@ -801,6 +801,9 @@ public final class ContactListNode: ASDisplayNode {
public var selectionState: ContactListNodeGroupSelectionState? {
return self.selectionStateValue
}
public var selectionStateSignal: Signal<ContactListNodeGroupSelectionState?, NoError> {
return self.selectionStatePromise.get()
}
public var selectionStateUpdated: ((ContactListNodeGroupSelectionState?) -> Void)?
public var selectedPeers: [ContactListPeer] {

View file

@ -1440,13 +1440,17 @@ public struct CombinedTransition {
}
public extension ContainedViewLayoutTransition {
func animateView(_ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
func animateView(allowUserInteraction: Bool = false, _ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
f()
completion?(true)
case let .animated(duration, curve):
UIView.animate(withDuration: duration, delay: 0.0, options: curve.viewAnimationOptions, animations: {
var options = curve.viewAnimationOptions
if allowUserInteraction {
options.insert(.allowUserInteraction)
}
UIView.animate(withDuration: duration, delay: 0.0, options: options, animations: {
f()
}, completion: completion)
}

View file

@ -68,13 +68,15 @@ public struct GridNodeLayout: Equatable {
public let scrollIndicatorInsets: UIEdgeInsets?
public let preloadSize: CGFloat
public let type: GridNodeLayoutType
public let cutout: CGRect?
public init(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets? = nil, preloadSize: CGFloat, type: GridNodeLayoutType) {
public init(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets? = nil, preloadSize: CGFloat, type: GridNodeLayoutType, cutout: CGRect? = nil) {
self.size = size
self.insets = insets
self.scrollIndicatorInsets = scrollIndicatorInsets
self.preloadSize = preloadSize
self.type = type
self.cutout = cutout
}
}
@ -515,7 +517,6 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
keepSection = false
}
if !previousFillsRow && item.fillsRowWithDynamicHeight != nil {
keepSection = false
}
@ -559,6 +560,10 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
}
}
if let cutout = self.gridLayout.cutout, cutout.intersects(CGRect(origin: nextItemOrigin, size: itemSize)) {
nextItemOrigin.x += cutout.width + itemSpacing
}
if !incrementedCurrentRow {
incrementedCurrentRow = true
contentSize.height += itemSize.height + lineSpacing

View file

@ -210,5 +210,13 @@ public class ImageNode: ASDisplayNode {
self.contents = nil
self.disposable.set(nil)
}
public var image: UIImage? {
if let contents = self.contents {
return UIImage(cgImage: contents as! CGImage)
} else {
return nil
}
}
}

View file

@ -623,8 +623,10 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
(self.supernode as? ListView)?.ensureItemNodeVisible(self, animated: false, overflow: 22.0, allowIntersection: true)
}
public func updateFrame(_ frame: CGRect, within containerSize: CGSize) {
self.frame = frame
public func updateFrame(_ frame: CGRect, within containerSize: CGSize, updateFrame: Bool = true) {
if updateFrame {
self.frame = frame
}
if frame.maxY < 0.0 || frame.minY > containerSize.height {
} else {
self.updateAbsoluteRect(frame, within: containerSize)

View file

@ -1104,7 +1104,7 @@ open class NavigationBar: ASDisplayNode {
}
} else if self.leftButtonNode.supernode != nil {
let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
leftTitleInset += leftButtonSize.width + leftButtonInset + 1.0
leftTitleInset = leftButtonSize.width + leftButtonInset + 1.0
self.leftButtonNode.alpha = 1.0
transition.updateFrame(node: self.leftButtonNode, frame: CGRect(origin: CGPoint(x: leftButtonInset, y: contentVerticalOrigin + floor((nominalHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize))
@ -1116,7 +1116,7 @@ open class NavigationBar: ASDisplayNode {
if self.rightButtonNode.supernode != nil {
let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape)
rightTitleInset += rightButtonSize.width + leftButtonInset + 1.0
rightTitleInset = rightButtonSize.width + leftButtonInset + 1.0
self.rightButtonNode.alpha = 1.0
transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize))
}
@ -1456,7 +1456,8 @@ open class NavigationBar: ASDisplayNode {
return nil
}
if result == self.view || result == self.buttonsContainerNode.view {
//result == self.view ||
if result == self.buttonsContainerNode.view {
return nil
}

View file

@ -204,15 +204,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
return .theme(file)
} else if ext == "wav" || ext == "opus" {
return .audio(file)
} else if ext == "json", let fileSize = file.size, fileSize < 1024 * 1024 {
if let path = context.account.postbox.mediaBox.completedResourcePath(file.resource), let composition = LOTComposition(filePath: path), composition.timeDuration > 0.0 {
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? ChatLocation.peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
return .gallery(.single(gallery))
}
}
if ext == "mkv" {
return .document(file, true)
}

View file

@ -301,7 +301,7 @@ struct InviteRequestsSearchContainerTransition {
let query: String
}
private func InviteRequestsSearchContainerPreparedRecentTransition(from fromEntries: [InviteRequestsSearchEntry], to toEntries: [InviteRequestsSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: InviteRequestsSearchContainerInteraction) -> InviteRequestsSearchContainerTransition {
private func inviteRequestsSearchContainerPreparedRecentTransition(from fromEntries: [InviteRequestsSearchEntry], to toEntries: [InviteRequestsSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: InviteRequestsSearchContainerInteraction) -> InviteRequestsSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
@ -534,7 +534,7 @@ public final class InviteRequestsSearchContainerNode: SearchDisplayControllerCon
let previousEntries = previousSearchItems.swap(entries)
updateActivity(false)
let firstTime = previousEntries == nil
let transition = InviteRequestsSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction)
let transition = inviteRequestsSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))

View file

@ -671,7 +671,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
strongSelf.addSubnode(strongSelf.bottomStripeNode)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
@ -693,10 +693,10 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
strongSelf.addSubnode(strongSelf.bottomStripeNode)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
@ -764,7 +764,11 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
strongSelf.addSubnode(shimmerNode)
if strongSelf.bottomStripeNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.bottomStripeNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if let (rect, size) = strongSelf.absoluteLocation {

View file

@ -111,7 +111,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let leftInset: CGFloat = 15.0
let topInset: CGFloat = 7.0
var bottomInset: CGFloat = 7.0
@ -130,7 +130,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
return (TelegramTextAttributes.URL, contents)
}))
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
@ -152,7 +152,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size)
}
})
}

View file

@ -145,6 +145,7 @@
#import <LegacyComponents/TGMediaOriginInfo.h>
#import <LegacyComponents/TGMediaPickerGalleryInterfaceView.h>
#import <LegacyComponents/TGMediaPickerGalleryModel.h>
#import <LegacyComponents/TGMediaPickerGallerySelectedItemsModel.h>
#import <LegacyComponents/TGMediaSelectionContext.h>
#import <LegacyComponents/TGMediaVideoConverter.h>
#import <LegacyComponents/TGMemoryImageCache.h>

View file

@ -20,6 +20,8 @@
- (void)resumePreview;
- (void)pausePreview;
- (void)removeCorners;
- (void)setZoomedProgress:(CGFloat)progress;
- (void)saveStartImage:(void (^)(void))completion;

View file

@ -51,6 +51,10 @@
- (SSignal *)imageSignalForItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)imageSignalForItem:(NSObject<TGMediaEditableItem> *)item withUpdates:(bool)withUpdates;
- (SSignal *)thumbnailImageSignalForIdentifier:(NSString *)identifier;
- (SSignal *)thumbnailImageSignalForIdentifier:(NSString *)identifier withUpdates:(bool)withUpdates synchronous:(bool)synchronous;
- (SSignal *)thumbnailImageSignalForItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)thumbnailImageSignalForItem:(id<TGMediaEditableItem>)item withUpdates:(bool)withUpdates synchronous:(bool)synchronous;
- (SSignal *)fastImageSignalForItem:(NSObject<TGMediaEditableItem> *)item withUpdates:(bool)withUpdates;

View file

@ -29,6 +29,8 @@
- (bool)toggleItemSelection:(id<TGMediaSelectableItem>)item success:(bool *)success;
- (bool)toggleItemSelection:(id<TGMediaSelectableItem>)item animated:(bool)animated sender:(id)sender success:(bool *)success;
- (void)moveItem:(id<TGMediaSelectableItem>)item toIndex:(NSUInteger)index;
- (void)clear;
- (bool)isItemSelected:(id<TGMediaSelectableItem>)item;

View file

@ -6,6 +6,9 @@
- (void)setImage:(UIImage *)image forKey:(NSString *)key attributes:(NSDictionary *)attributes;
- (UIImage *)imageForKey:(NSString *)key attributes:(__autoreleasing NSDictionary **)attributes;
- (void)imageForKey:(NSString *)key attributes:(__autoreleasing NSDictionary **)attributes completion:(void (^)(UIImage *))completion;
- (void)setAverageColor:(uint32_t)color forKey:(NSString *)key;
- (bool)averageColorForKey:(NSString *)key color:(uint32_t *)color;
- (void)clearCache;

View file

@ -1,12 +0,0 @@
#import "TGAttachmentMenuCell.h"
#import "TGAttachmentCameraView.h"
@interface TGAttachmentCameraCell : TGAttachmentMenuCell
@property (nonatomic, readonly) TGAttachmentCameraView *cameraView;
- (void)attachCameraViewIfNeeded:(TGAttachmentCameraView *)cameraView;
@end
extern NSString *const TGAttachmentCameraCellIdentifier;

View file

@ -1,18 +0,0 @@
#import "TGAttachmentCameraCell.h"
NSString *const TGAttachmentCameraCellIdentifier = @"AttachmentCameraCell";
@implementation TGAttachmentCameraCell
- (void)attachCameraViewIfNeeded:(TGAttachmentCameraView *)cameraView
{
if (_cameraView == cameraView)
return;
_cameraView = cameraView;
_cameraView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_cameraView.frame = self.bounds;
[self addSubview:cameraView];
}
@end

View file

@ -1,4 +1,5 @@
#import "TGAttachmentCameraView.h"
#import "TGImageUtils.h"
#import "LegacyComponentsInternal.h"
@ -14,6 +15,7 @@
#import <AVFoundation/AVFoundation.h>
@interface TGAttachmentCameraView ()
{
UIView *_wrapperView;
@ -52,7 +54,7 @@
[_wrapperView addSubview:_previewView];
[camera attachPreviewView:_previewView];
_iconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Editor/Camera"]];
_iconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Chat/Attach Menu/Camera"]];
[self addSubview:_iconView];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)]];
@ -121,6 +123,10 @@
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
}
- (void)removeCorners {
[_cornersView removeFromSuperview];
}
- (void)setPallete:(TGMenuSheetPallete *)pallete
{
_pallete = pallete;
@ -228,6 +234,7 @@
void(^block)(void) = ^
{
_wrapperView.transform = CGAffineTransformMakeRotation(-1 * TGRotationForInterfaceOrientation(orientation));
_wrapperView.frame = self.bounds;
};
if (animated)
@ -240,11 +247,13 @@
{
[super layoutSubviews];
_wrapperView.frame = self.bounds;
TGCameraPreviewView *previewView = _previewView;
if (previewView.superview == _wrapperView)
previewView.frame = self.bounds;
_iconView.frame = CGRectMake((self.frame.size.width - _iconView.frame.size.width) / 2, (self.frame.size.height - _iconView.frame.size.height) / 2, _iconView.frame.size.width, _iconView.frame.size.height);
_iconView.frame = CGRectMake(self.frame.size.width - _iconView.frame.size.width - 3.0, 3.0 - TGScreenPixel, _iconView.frame.size.width, _iconView.frame.size.height);
}
- (void)saveStartImage:(void (^)(void))completion {

View file

@ -147,11 +147,14 @@
if (_cachedDuration == nil)
{
return [[TGMediaAssetImageSignals avAssetForVideoAsset:self] map:^id(AVAsset *asset)
NSTimeInterval assetDuration = self.videoDuration;
return [[[TGMediaAssetImageSignals avAssetForVideoAsset:self] map:^id(AVAsset *asset)
{
NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
_cachedDuration = @(duration);
return _cachedDuration;
}] catch:^SSignal * _Nonnull(id _Nullable error) {
return [SSignal single:@(assetDuration)];
}];
}

View file

@ -173,7 +173,9 @@
groupSignal = [SSignal single:_assetGroup];
} else {
groupSignal = [_assetsLibrary cameraRollGroup];
reversed = true;
if (_intent == TGMediaAssetsControllerSendMediaIntent) {
reversed = true;
}
}
__weak TGMediaAssetsPickerController *weakSelf = self;

View file

@ -240,18 +240,26 @@
- (SSignal *)thumbnailImageSignalForItem:(id<TGMediaEditableItem>)item
{
return [self thumbnailImageSignalForItem:item withUpdates:true synchronous:false];
return [self thumbnailImageSignalForIdentifier:item.uniqueIdentifier];
}
- (SSignal *)thumbnailImageSignalForItem:(id<TGMediaEditableItem>)item withUpdates:(bool)withUpdates synchronous:(bool)synchronous
{
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
return [self thumbnailImageSignalForIdentifier:item.uniqueIdentifier withUpdates:withUpdates synchronous: synchronous];
}
- (SSignal *)thumbnailImageSignalForIdentifier:(NSString *)identifier {
return [self thumbnailImageSignalForIdentifier:identifier withUpdates:true synchronous:false];
}
- (SSignal *)thumbnailImageSignalForIdentifier:(NSString *)identifier withUpdates:(bool)withUpdates synchronous:(bool)synchronous {
NSString *itemId = [self _contextualIdForItemId:identifier];
if (itemId == nil)
return [SSignal fail:nil];
SSignal *updateSignal = [[_thumbnailImagePipe.signalProducer() filter:^bool(TGMediaImageUpdate *update)
{
return [update.item.uniqueIdentifier isEqualToString:item.uniqueIdentifier];
return [update.item.uniqueIdentifier isEqualToString:identifier];
}] map:^id(TGMediaImageUpdate *update)
{
return update.representation;
@ -290,25 +298,35 @@
SSignal *signal = [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
UIImage *result = [imageCache imageForKey:itemId attributes:NULL];
if (result == nil)
{
NSData *imageData = [_diskCache getValueForKey:[imageDiskUri dataUsingEncoding:NSUTF8StringEncoding]];
if (imageData.length > 0)
void (^completionBlock)(UIImage *) = ^(UIImage *result) {
if (result == nil)
{
result = [UIImage imageWithData:imageData];
[imageCache setImage:result forKey:itemId attributes:NULL];
NSData *imageData = [_diskCache getValueForKey:[imageDiskUri dataUsingEncoding:NSUTF8StringEncoding]];
if (imageData.length > 0)
{
result = [UIImage imageWithData:imageData];
[imageCache setImage:result forKey:itemId attributes:NULL];
}
}
}
if (result != nil)
{
[subscriber putNext:result];
[subscriber putCompletion];
}
else
{
[subscriber putError:nil];
}
};
if (result != nil)
{
[subscriber putNext:result];
[subscriber putCompletion];
}
else
{
[subscriber putError:nil];
if (synchronous) {
UIImage *result = [imageCache imageForKey:itemId attributes:NULL];
completionBlock(result);
} else {
[imageCache imageForKey:itemId attributes:NULL completion:^(UIImage *result) {
completionBlock(result);
}];
}
return nil;

View file

@ -159,6 +159,7 @@
backingItem.editingContext = self.editingContext;
backingItem.stickersContext = self.stickersContext;
backingItem.asFile = self.asFile;
backingItem.immediateThumbnailImage = self.immediateThumbnailImage;
_backingItem = backingItem;
}
return _backingItem;

View file

@ -385,16 +385,27 @@
}]];
}
- (id<TGModernGalleryItem>)item {
if (_fetchItem != nil) {
return _fetchItem;
} else {
return _item;
}
}
- (void)setItem:(TGMediaPickerGalleryVideoItem *)item synchronously:(bool)synchronously
{
TGMediaPickerGalleryFetchResultItem *fetchItem;
if ([item isKindOfClass:[TGMediaPickerGalleryFetchResultItem class]]) {
_fetchItem = (TGMediaPickerGalleryFetchResultItem *)item;
item = (TGMediaPickerGalleryVideoItem *)[_fetchItem backingItem];
fetchItem = (TGMediaPickerGalleryFetchResultItem *)item;
item = (TGMediaPickerGalleryVideoItem *)[fetchItem backingItem];
}
bool itemChanged = ![item isEqual:self.item];
bool itemIdChanged = item.uniqueId != self.item.uniqueId;
_fetchItem = fetchItem;
[super setItem:item synchronously:synchronously];
if (itemIdChanged) {

View file

@ -151,6 +151,15 @@
return newValue;
}
- (void)moveItem:(id<TGMediaSelectableItem>)item toIndex:(NSUInteger)index {
NSUInteger sourceIndex = [self indexOfItem:item] - 1;
[_selectedIdentifiers removeObjectAtIndex:sourceIndex];
[_selectedIdentifiers insertObject:item.uniqueIdentifier atIndex:index - 1];
_pipe.sink([TGMediaSelectionChange changeWithItem:item selected:true animated:false sender:nil]);
}
- (SSignal *)itemSelectedSignal:(id<TGMediaSelectableItem>)item
{
return [[self itemInformativeSelectedSignal:item] map:^NSNumber *(TGMediaSelectionChange *change)

View file

@ -121,6 +121,28 @@
return result;
}
- (void)imageForKey:(NSString *)key attributes:(__autoreleasing NSDictionary **)attributes completion:(void (^)(UIImage *))completion {
if (key == nil) {
completion(nil);
return;
}
__block id result = nil;
[_queue dispatch:^
{
TGMemoryImageCacheItem *item = _cache[key];
if (item != nil)
{
item.timestamp = CFAbsoluteTimeGetCurrent();
if (attributes != NULL)
*attributes = item.attributes;
completion(item.object);
}
}];
}
- (void)setImage:(UIImage *)image forKey:(NSString *)key attributes:(NSDictionary *)attributes
{
if (key != nil)

View file

@ -104,7 +104,7 @@ public class LegacyAssetPickerContext: AttachmentMediaPickerContext {
self.controller?.editingContext.setForcedCaption(caption, skipUpdate: true)
}
public func send(silently: Bool) {
public func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
self.controller?.send(silently)
}

View file

@ -36,6 +36,7 @@ swift_library(
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/WallpaperResources:WallpaperResources",
"//submodules/Postbox:Postbox",
"//submodules/ShimmerEffect:ShimmerEffect",
],
visibility = [
"//visibility:public",

View file

@ -19,13 +19,14 @@ import UniversalMediaPlayer
import ContextUI
import FileMediaResourceStatus
import ManagedAnimationNode
import ShimmerEffect
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
private let redColors: (UInt32, UInt32) = (0xf0625d, 0xde524e)
private let greenColors: (UInt32, UInt32) = (0x72ce76, 0x54b658)
private let blueColors: (UInt32, UInt32) = (0x60b0e8, 0x4597d1)
private let yellowColors: (UInt32, UInt32) = (0xf5c565, 0xe5a64e)
private let redColors: (UInt32, UInt32) = (0xed6b7b, 0xe63f45)
private let greenColors: (UInt32, UInt32) = (0x99de6f, 0x5fb84f)
private let blueColors: (UInt32, UInt32) = (0x72d5fd, 0x2a9ef1)
private let yellowColors: (UInt32, UInt32) = (0xffa24b, 0xed705c)
private let extensionColorsMap: [String: (UInt32, UInt32)] = [
"ppt": redColors,
@ -47,14 +48,20 @@ private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: colors.0).cgColor)
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 Z ")
context.saveGState()
context.beginPath()
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
context.clip()
context.setFillColor(UIColor(rgb: colors.0).withMultipliedBrightnessBy(0.85).cgColor)
let gradientColors = [UIColor(rgb: colors.0).cgColor, UIColor(rgb: colors.1).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
context.restoreGState()
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.2).cgColor)
context.translateBy(x: 40.0 - 14.0, y: 0.0)
let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ")
})
@ -88,6 +95,8 @@ private func extensionImage(fileExtension: String?) -> UIImage? {
}
}
private let extensionFont = Font.with(size: 15.0, design: .round, weight: .bold)
private let mediumExtensionFont = Font.with(size: 14.0, design: .round, weight: .bold)
private let smallExtensionFont = Font.with(size: 12.0, design: .round, weight: .bold)
private struct FetchControls {
let fetch: () -> Void
@ -158,6 +167,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
private var backgroundNode: ASDisplayNode?
private let highlightedBackgroundNode: ASDisplayNode
public let separatorNode: ASDisplayNode
private let maskNode: ASImageNode
private var selectionNode: ItemListSelectableControlNode?
@ -188,6 +198,9 @@ public final class ListMessageFileItemNode: ListMessageNode {
private var downloadStatusIconNode: DownloadIconNode?
private var linearProgressNode: LinearProgressNode?
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var context: AccountContext?
private (set) var message: Message?
@ -207,6 +220,9 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.separatorNode.displaysAsynchronously = false
self.separatorNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
@ -276,13 +292,13 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.addSubnode(self.separatorNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
guard let strongSelf = self, let item = strongSelf.item, let message = item.message else {
return
}
cancelParentGestures(view: strongSelf.view)
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
item.interaction.openMessageContextMenu(message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
@ -344,6 +360,15 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode)
let textNodeMakeLayout = TextNode.asyncLayout(self.textNode)
@ -361,7 +386,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode)
return { [weak self] item, params, _, _, dateHeaderAtBottom in
return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
@ -404,134 +429,163 @@ public final class ListMessageFileItemNode: ListMessageNode {
let message = item.message
var selectedMedia: Media?
for media in message.media {
if let file = media as? TelegramMediaFile {
selectedMedia = file
isInstantVideo = file.isInstantVideo
for attribute in file.attributes {
if case let .Audio(voice, duration, title, performer, _) = attribute {
isAudio = true
isVoice = voice
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var descriptionString: String
if let performer = performer {
if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = performer
if let message = message {
for media in message.media {
if let file = media as? TelegramMediaFile {
selectedMedia = file
isInstantVideo = file.isInstantVideo
for attribute in file.attributes {
if case let .Audio(voice, duration, title, performer, _) = attribute {
isAudio = true
isVoice = voice
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var descriptionString: String
if let performer = performer {
if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = performer
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(performer)"
}
} else if let size = file.size {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(performer)"
descriptionString = ""
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty {
descriptionString = authorString
} else {
descriptionString = "\(descriptionString)\(authorString)"
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
if !voice {
if file.fileName?.lowercased().hasSuffix(".ogg") == true {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: "", performer: "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: "", performer: "", isThumbnail: false)))
} else {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false)))
}
} else {
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
}
}
if isInstantVideo || isVoice {
var authorName: String
if let author = message.forwardInfo?.author {
if author.id == item.context.account.peerId {
authorName = item.presentationData.strings.DialogList_You
} else {
authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} else if let signature = message.forwardInfo?.authorSignature {
authorName = signature
} else if let author = message.author {
if author.id == item.context.account.peerId {
authorName = item.presentationData.strings.DialogList_You
} else {
authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} else if let size = file.size {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = ""
authorName = " "
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
authorName = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
}
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if let duration = file.duration {
if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo {
descriptionString = stringForDuration(Int32(duration))
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
}
} else {
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = dateString
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
iconImage = .roundVideo(file)
} else if !isAudio {
let fileName: String = file.fileName ?? "File"
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var fileExtension: String?
if let range = fileName.range(of: ".", options: [.backwards]) {
fileExtension = fileName[range.upperBound...].lowercased()
}
extensionIconImage = extensionImage(fileExtension: fileExtension)
if let fileExtension = fileExtension {
extensionText = NSAttributedString(string: fileExtension, font: fileExtension.count > 3 ? mediumExtensionFont : extensionFont, textColor: UIColor.white)
}
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImage = .imageRepresentation(file, representation)
}
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if let size = file.size {
if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)"
}
} else {
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)"
}
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty {
descriptionString = authorString
} else {
descriptionString = "\(descriptionString)\(authorString)"
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
if !voice {
if file.fileName?.lowercased().hasSuffix(".ogg") == true {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: "", performer: "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: "", performer: "", isThumbnail: false)))
} else {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false)))
}
} else {
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: item.message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
}
}
if isInstantVideo || isVoice {
var authorName: String
if let author = message.forwardInfo?.author {
if author.id == item.context.account.peerId {
authorName = item.presentationData.strings.DialogList_You
} else {
authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} else if let signature = message.forwardInfo?.authorSignature {
authorName = signature
} else if let author = message.author {
if author.id == item.context.account.peerId {
authorName = item.presentationData.strings.DialogList_You
} else {
authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} else {
authorName = " "
}
if item.isGlobalSearchResult || item.isDownloadList {
authorName = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
}
break
} else if let image = media as? TelegramMediaImage {
selectedMedia = image
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if let duration = file.duration {
if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = stringForDuration(Int32(duration))
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
}
} else {
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = dateString
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
iconImage = .roundVideo(file)
} else if !isAudio {
var fileName: String = file.fileName ?? "File"
if file.isVideo {
fileName = item.presentationData.strings.Message_Video
}
//TODO:localize
let fileName: String = "Photo"
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var fileExtension: String?
if let range = fileName.range(of: ".", options: [.backwards]) {
fileExtension = fileName[range.upperBound...].lowercased()
}
extensionIconImage = extensionImage(fileExtension: fileExtension)
if let fileExtension = fileExtension {
extensionText = NSAttributedString(string: fileExtension, font: extensionFont, textColor: UIColor.white)
if let representation = smallestImageRepresentation(image.representations) {
iconImage = .imageRepresentation(image, representation)
}
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImage = .imageRepresentation(file, representation)
}
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if let size = file.size {
if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)"
}
} else {
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)"
}
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)"
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty {
descriptionString = authorString
} else {
@ -541,43 +595,17 @@ public final class ListMessageFileItemNode: ListMessageNode {
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
break
} else if let image = media as? TelegramMediaImage {
selectedMedia = image
let fileName: String = item.presentationData.strings.Message_Video
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if let representation = smallestImageRepresentation(image.representations) {
iconImage = .imageRepresentation(image, representation)
}
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)"
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if descriptionString.isEmpty {
descriptionString = authorString
} else {
descriptionString = "\(descriptionString)\(authorString)"
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
}
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
isRestricted = true
break
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
isRestricted = true
break
}
}
} else {
titleText = NSAttributedString(string: " ", font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
var mediaUpdated = false
@ -592,11 +620,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
var statusUpdated = mediaUpdated
if currentMessage?.id != message.id || currentMessage?.flags != message.flags {
if currentMessage?.id != message?.id || currentMessage?.flags != message?.flags {
statusUpdated = true
}
if let selectedMedia = selectedMedia {
if let message = message, let selectedMedia = selectedMedia {
if mediaUpdated {
let context = item.context
updatedFetchControls = FetchControls(fetch: { [weak self] in
@ -624,7 +652,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
})
}
if statusUpdated {
if statusUpdated && item.displayFileInfo {
if let file = selectedMedia as? TelegramMediaFile {
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
@ -669,7 +697,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
var chatListSearchResult: CachedChatListSearchResult?
let messageText = foldLineBreaks(item.message.text)
let messageText = foldLineBreaks(item.message?.text ?? "")
if let searchQuery = item.interaction.searchTextHighightState {
if let cached = currentSearchResult, cached.matches(text: messageText, searchQuery: searchQuery) {
@ -709,7 +737,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -720,7 +748,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
if extensionTextLayout.truncated, let text = extensionText?.string {
extensionText = NSAttributedString(string: text, font: smallExtensionFont, textColor: .white, paragraphAlignment: .center)
(extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
var iconImageApply: (() -> Void)?
if let iconImage = iconImage {
@ -743,24 +775,26 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
}
if currentIconImage != iconImage {
if let iconImage = iconImage {
switch iconImage {
case let .imageRepresentation(media, representation):
if let file = media as? TelegramMediaFile {
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, mediaReference: FileMediaReference.message(message: MessageReference(message), media: file).abstract, representation: representation)
} else if let image = media as? TelegramMediaImage {
updateIconImageSignal = mediaGridMessagePhoto(account: item.context.account, photoReference: ImageMediaReference.message(message: MessageReference(message), media: image))
} else {
updateIconImageSignal = .complete()
}
case let .albumArt(file, albumArt):
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, engine: item.context.engine, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.presentationData.theme.theme.list.itemAccentColor)
case let .roundVideo(file):
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3))
if let message = message {
if currentIconImage != iconImage {
if let iconImage = iconImage {
switch iconImage {
case let .imageRepresentation(media, representation):
if let file = media as? TelegramMediaFile {
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, mediaReference: FileMediaReference.message(message: MessageReference(message), media: file).abstract, representation: representation)
} else if let image = media as? TelegramMediaImage {
updateIconImageSignal = mediaGridMessagePhoto(account: item.context.account, photoReference: ImageMediaReference.message(message: MessageReference(message), media: image))
} else {
updateIconImageSignal = .complete()
}
case let .albumArt(file, albumArt):
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, engine: item.context.engine, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.presentationData.theme.theme.list.itemAccentColor)
case let .roundVideo(file):
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3))
}
} else {
updateIconImageSignal = .complete()
}
} else {
updateIconImageSignal = .complete()
}
}
@ -768,6 +802,9 @@ public final class ListMessageFileItemNode: ListMessageNode {
if dateHeaderAtBottom, let header = item.header {
insets.top += header.height
}
if !mergedBottom, case .blocks = item.style {
insets.bottom += 35.0
}
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 8.0 * 2.0 + titleNodeLayout.size.height + 3.0 + descriptionNodeLayout.size.height + (textNodeLayout.size.height > 0.0 ? textNodeLayout.size.height + 3.0 : 0.0)), insets: insets)
@ -778,7 +815,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
let transition: ContainedViewLayoutTransition
if animation.isAnimated {
if animation.isAnimated && currentItem?.message != nil {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
@ -803,8 +840,9 @@ public final class ListMessageFileItemNode: ListMessageNode {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.containerNode.isGestureEnabled = item.displayFileInfo
strongSelf.currentIsRestricted = isRestricted
strongSelf.currentIsRestricted = isRestricted || item.message == nil
strongSelf.currentMedia = selectedMedia
strongSelf.message = message
strongSelf.context = item.context
@ -855,11 +893,41 @@ public final class ListMessageFileItemNode: ListMessageNode {
})
}
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: nodeLayout.contentSize.height + UIScreenPixel))
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height))
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.contentSize.height))
}
switch item.style {
case .plain:
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
case .blocks:
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
if !mergedTop {
hasTopCorners = true
}
if !mergedBottom {
hasBottomCorners = true
strongSelf.separatorNode.isHidden = hasCorners
} else {
strongSelf.separatorNode.isHidden = false
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
if let backgroundNode = strongSelf.backgroundNode {
strongSelf.maskNode.frame = backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
}
}
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
@ -890,17 +958,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - rightInset - dateNodeLayout.size.width, y: 11.0), size: dateNodeLayout.size))
strongSelf.dateNode.isHidden = !item.isGlobalSearchResult
let iconFrame: CGRect
if isAudio {
let iconSize = CGSize(width: 40.0, height: 40.0)
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
} else {
let iconSize = CGSize(width: 40.0, height: 40.0)
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
}
let iconSize = CGSize(width: 40.0, height: 40.0)
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
strongSelf.extensionIconNode.image = extensionIconImage
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - extensionTextLayout.size.width) / 2.0), y: iconFrame.minY + 2.0 + floor((iconFrame.height - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - extensionTextLayout.size.width) / 2.0), y: iconFrame.minY + 7.0 + floor((iconFrame.height - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
transition.updateFrame(node: strongSelf.iconStatusNode, frame: iconFrame)
@ -968,6 +1030,44 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
strongSelf.updateStatus(transition: transition)
if item.message == nil {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.separatorNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.separatorNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 120.0
let descriptionLineWidth: CGFloat = 60.0
let lineDiameter: CGFloat = 8.0
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
let descriptionFrame = strongSelf.descriptionNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: descriptionFrame.minX, y: descriptionFrame.minY + floor((descriptionFrame.height - lineDiameter) / 2.0)), width: descriptionLineWidth, diameter: lineDiameter))
shapes.append(.roundedRect(rect: iconFrame, cornerRadius: 6.0))
shimmerNode.update(backgroundColor: item.presentationData.theme.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: nodeLayout.contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
}
})
}
@ -1010,7 +1110,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
switch fetchStatus {
case let .Fetching(_, progress):
if item.isDownloadList {
iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: false, appearance: nil)
iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: true, appearance: nil)
}
case .Local:
if isAudio || isInstantVideo {
@ -1042,7 +1142,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
if highlighted, let item = self.item, case .none = item.selection {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
if let backgroundNode = self.backgroundNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: backgroundNode)
} else {
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
@ -1063,7 +1167,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
override public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil {
if let item = self.item, let message = item.message, message.id == id, self.iconImageNode.supernode != nil {
let iconImageNode = self.iconImageNode
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
return (iconImageNode?.view.snapshotContentTree(unhide: true), nil)
@ -1073,7 +1177,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
override public func updateHiddenMedia() {
if let interaction = self.interaction, let item = self.item, interaction.getHiddenMedia()[item.message.id] != nil {
if let interaction = self.interaction, let item = self.item, let message = item.message, interaction.getHiddenMedia()[message.id] != nil {
self.iconImageNode.isHidden = true
} else {
self.iconImageNode.isHidden = false
@ -1230,8 +1334,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
fetch()
}
case .Local:
if let item = self.item, let interaction = self.interaction {
let _ = interaction.openMessage(item.message, .default)
if let item = self.item, let message = item.message, let interaction = self.interaction {
let _ = interaction.openMessage(message, .default)
}
}
case .playbackStatus:

View file

@ -8,6 +8,7 @@ import Postbox
import TelegramPresentationData
import AccountContext
import TelegramUIPreferences
import ItemListUI
public final class ListMessageItemInteraction {
public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
@ -47,18 +48,20 @@ public final class ListMessageItem: ListViewItem {
let context: AccountContext
let chatLocation: ChatLocation
let interaction: ListMessageItemInteraction
let message: Message
let message: Message?
public let selection: ChatHistoryMessageSelection
let hintIsLink: Bool
let isGlobalSearchResult: Bool
let isDownloadList: Bool
let displayFileInfo: Bool
let displayBackground: Bool
let style: ItemListStyle
let header: ListViewItemHeader?
public let selectable: Bool = true
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayBackground: Bool = false) {
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
self.presentationData = presentationData
self.context = context
self.chatLocation = chatLocation
@ -66,7 +69,7 @@ public final class ListMessageItem: ListViewItem {
self.message = message
if let header = customHeader {
self.header = header
} else if displayHeader {
} else if displayHeader, let message = message {
self.header = ListMessageDateHeader(timestamp: message.timestamp, theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize)
} else {
self.header = nil
@ -75,21 +78,27 @@ public final class ListMessageItem: ListViewItem {
self.hintIsLink = hintIsLink
self.isGlobalSearchResult = isGlobalSearchResult
self.isDownloadList = isDownloadList
self.displayFileInfo = displayFileInfo
self.displayBackground = displayBackground
self.style = style
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
var viewClassName: AnyClass = ListMessageSnippetItemNode.self
if !self.hintIsLink {
for media in self.message.media {
if let _ = media as? TelegramMediaFile {
viewClassName = ListMessageFileItemNode.self
break
} else if let _ = media as? TelegramMediaImage {
viewClassName = ListMessageFileItemNode.self
break
if let message = self.message {
for media in message.media {
if let _ = media as? TelegramMediaFile {
viewClassName = ListMessageFileItemNode.self
break
} else if let _ = media as? TelegramMediaImage {
viewClassName = ListMessageFileItemNode.self
break
}
}
} else {
viewClassName = ListMessageFileItemNode.self
}
}
@ -99,7 +108,7 @@ public final class ListMessageItem: ListViewItem {
node.setupItem(self)
let nodeLayout = node.asyncLayout()
let (top, bottom, dateAtBottom) = (previousItem != nil, nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom)
node.updateSelectionState(animated: false)
@ -132,7 +141,7 @@ public final class ListMessageItem: ListViewItem {
let nodeLayout = nodeValue.asyncLayout()
async {
let (top, bottom, dateAtBottom) = (previousItem != nil, nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom)
Queue.mainQueue().async {
@ -150,17 +159,25 @@ public final class ListMessageItem: ListViewItem {
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
guard let message = self.message else {
return
}
if case let .selectable(selected) = self.selection {
self.interaction.toggleMessagesSelection([self.message.id], !selected)
self.interaction.toggleMessagesSelection([message.id], !selected)
} else {
listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageFileItemNode {
if let messageId = itemNode.item?.message.id, messageId == self.message.id {
itemNode.activateMedia()
}
} else if let itemNode = itemNode as? ListMessageSnippetItemNode {
if let messageId = itemNode.item?.message.id, messageId == self.message.id {
itemNode.activateMedia()
if !self.displayFileInfo {
let _ = self.interaction.openMessage(message, .default)
} else {
listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageFileItemNode {
if let messageId = itemNode.item?.message?.id, messageId == message.id {
itemNode.activateMedia()
}
} else if let itemNode = itemNode as? ListMessageSnippetItemNode {
if let messageId = itemNode.item?.message?.id, messageId == message.id {
itemNode.activateMedia()
}
}
}
}
@ -181,6 +198,10 @@ public final class ListMessageItem: ListViewItem {
}
public var description: String {
return "(ListMessageItem id: \(self.message.id), text: \"\(self.message.text)\")"
if let message = self.message {
return "(ListMessageItem id: \(message.id), text: \"\(message.text)\")"
} else {
return "(ListMessageItem empty)"
}
}
}

View file

@ -122,11 +122,11 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
self.offsetContainerNode.addSubnode(self.authorNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
guard let strongSelf = self, let item = strongSelf.item, let message = item.message else {
return
}
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
item.interaction.openMessageContextMenu(message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
@ -259,209 +259,212 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
var selectedMedia: TelegramMediaWebpage?
var processed = false
for media in item.message.media {
if let webpage = media as? TelegramMediaWebpage {
selectedMedia = webpage
if case let .Loaded(content) = webpage.content {
if content.instantPage != nil && instantPageType(of: content) != .album {
isInstantView = true
}
let (parsedUrl, _) = parseUrl(url: content.url, wasConcealed: false)
primaryUrl = parsedUrl
processed = true
var hostName: String = ""
if let url = URL(string: parsedUrl), let host = url.host, !host.isEmpty {
hostName = host
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
}
title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if let image = content.image {
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: image), representation)
}
} else if let file = content.file {
if content.type == "telegram_background" {
if let wallpaper = parseWallpaperUrl(content.url) {
switch wallpaper {
case let .slug(slug, _, colors, intensity, angle):
previewWallpaperFileReference = .message(message: MessageReference(item.message), media: file)
previewWallpaper = .file(TelegramWallpaper.File(id: file.fileId.id, accessHash: 0, isCreator: false, isDefault: false, isPattern: true, isDark: false, slug: slug, file: file, settings: WallpaperSettings(blur: false, motion: false, colors: colors, intensity: intensity, rotation: angle)))
default:
break
}
}
}
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: file), representation)
}
}
let mutableDescriptionText = NSMutableAttributedString()
if let text = content.text, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let plainUrlString = NSAttributedString(string: content.url.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)
let urlString = NSMutableAttributedString()
urlString.append(plainUrlString)
urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.url, range: NSMakeRange(0, urlString.length))
linkText = urlString
descriptionText = mutableDescriptionText
}
break
}
}
if !processed {
var messageEntities: [MessageTextEntity]?
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
for media in item.message.media {
if let image = media as? TelegramMediaImage {
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: image), representation)
}
break
}
if let file = media as? TelegramMediaFile {
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: file), representation)
}
break
}
}
var entities: [MessageTextEntity]?
entities = messageEntities
if entities == nil {
let parsedEntities = generateTextEntities(item.message.text, enabledTypes: .all)
if !parsedEntities.isEmpty {
entities = parsedEntities
}
}
if let entities = entities {
loop: for entity in entities {
switch entity.type {
case .Url, .Email:
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let nsString = item.message.text as NSString
if range.location + range.length > nsString.length {
range.location = max(0, nsString.length - range.length)
range.length = nsString.length - range.location
if let message = item.message {
for media in message.media {
if let webpage = media as? TelegramMediaWebpage {
selectedMedia = webpage
if case let .Loaded(content) = webpage.content {
if content.instantPage != nil && instantPageType(of: content) != .album {
isInstantView = true
}
let (parsedUrl, _) = parseUrl(url: content.url, wasConcealed: false)
primaryUrl = parsedUrl
processed = true
var hostName: String = ""
if let url = URL(string: parsedUrl), let host = url.host, !host.isEmpty {
hostName = host
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
}
title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if let image = content.image {
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation)
}
let tempUrlString = nsString.substring(with: range)
var (urlString, concealed) = parseUrl(url: tempUrlString, wasConcealed: false)
let rawUrlString = urlString
var parsedUrl = URL(string: urlString)
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
urlString = "http://" + urlString
parsedUrl = URL(string: urlString)
} else if let file = content.file {
if content.type == "telegram_background" {
if let wallpaper = parseWallpaperUrl(content.url) {
switch wallpaper {
case let .slug(slug, _, colors, intensity, angle):
previewWallpaperFileReference = .message(message: MessageReference(message), media: file)
previewWallpaper = .file(TelegramWallpaper.File(id: file.fileId.id, accessHash: 0, isCreator: false, isDefault: false, isPattern: true, isDark: false, slug: slug, file: file, settings: WallpaperSettings(blur: false, motion: false, colors: colors, intensity: intensity, rotation: angle)))
default:
break
}
}
}
let host: String? = concealed ? urlString : parsedUrl?.host
if let url = parsedUrl, let host = host {
primaryUrl = urlString
if url.path.hasPrefix("/addstickers/") {
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation)
}
}
let mutableDescriptionText = NSMutableAttributedString()
if let text = content.text, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let plainUrlString = NSAttributedString(string: content.url.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)
let urlString = NSMutableAttributedString()
urlString.append(plainUrlString)
urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.url, range: NSMakeRange(0, urlString.length))
linkText = urlString
descriptionText = mutableDescriptionText
}
break
}
}
if !processed {
var messageEntities: [MessageTextEntity]?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
for media in message.media {
if let image = media as? TelegramMediaImage {
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation)
}
break
}
if let file = media as? TelegramMediaFile {
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation)
}
break
}
}
var entities: [MessageTextEntity]?
entities = messageEntities
if entities == nil {
let parsedEntities = generateTextEntities(message.text, enabledTypes: .all)
if !parsedEntities.isEmpty {
entities = parsedEntities
}
}
if let entities = entities {
loop: for entity in entities {
switch entity.type {
case .Url, .Email:
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let nsString = message.text as NSString
if range.location + range.length > nsString.length {
range.location = max(0, nsString.length - range.length)
range.length = nsString.length - range.location
}
let tempUrlString = nsString.substring(with: range)
var (urlString, concealed) = parseUrl(url: tempUrlString, wasConcealed: false)
let rawUrlString = urlString
var parsedUrl = URL(string: urlString)
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
urlString = "http://" + urlString
parsedUrl = URL(string: urlString)
}
let host: String? = concealed ? urlString : parsedUrl?.host
if let url = parsedUrl, let host = host {
primaryUrl = urlString
if url.path.hasPrefix("/addstickers/") {
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
title = NSAttributedString(string: host, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
}
let mutableDescriptionText = NSMutableAttributedString()
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
title = NSAttributedString(string: host, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
}
let mutableDescriptionText = NSMutableAttributedString()
let (messageTextUrl, _) = parseUrl(url: item.message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let urlAttributedString = NSMutableAttributedString()
urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
}
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
linkText = urlAttributedString
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let urlAttributedString = NSMutableAttributedString()
urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
}
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
linkText = urlAttributedString
descriptionText = mutableDescriptionText
}
break loop
case let .TextUrl(url):
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let nsString = item.message.text as NSString
if range.location + range.length > nsString.length {
range.location = max(0, nsString.length - range.length)
range.length = nsString.length - range.location
}
let tempTitleString = (nsString.substring(with: range) as String).trimmingCharacters(in: .whitespacesAndNewlines)
var (urlString, concealed) = parseUrl(url: url, wasConcealed: false)
let rawUrlString = urlString
var parsedUrl = URL(string: urlString)
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
urlString = "http://" + urlString
parsedUrl = URL(string: urlString)
}
let host: String? = concealed ? urlString : parsedUrl?.host
if let url = parsedUrl, let host = host {
primaryUrl = urlString
title = NSAttributedString(string: tempTitleString as String, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if url.path.hasPrefix("/addstickers/") {
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
descriptionText = mutableDescriptionText
}
let mutableDescriptionText = NSMutableAttributedString()
let (messageTextUrl, _) = parseUrl(url: item.message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
break loop
case let .TextUrl(url):
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let nsString = message.text as NSString
if range.location + range.length > nsString.length {
range.location = max(0, nsString.length - range.length)
range.length = nsString.length - range.location
}
let urlAttributedString = NSMutableAttributedString()
urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
let tempTitleString = (nsString.substring(with: range) as String).trimmingCharacters(in: .whitespacesAndNewlines)
var (urlString, concealed) = parseUrl(url: url, wasConcealed: false)
let rawUrlString = urlString
var parsedUrl = URL(string: urlString)
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
urlString = "http://" + urlString
parsedUrl = URL(string: urlString)
}
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
linkText = urlAttributedString
descriptionText = mutableDescriptionText
}
break loop
default:
break
let host: String? = concealed ? urlString : parsedUrl?.host
if let url = parsedUrl, let host = host {
primaryUrl = urlString
title = NSAttributedString(string: tempTitleString as String, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if url.path.hasPrefix("/addstickers/") {
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
}
let mutableDescriptionText = NSMutableAttributedString()
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let urlAttributedString = NSMutableAttributedString()
urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
}
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
linkText = urlAttributedString
descriptionText = mutableDescriptionText
}
break loop
default:
break
}
}
}
}
}
var chatListSearchResult: CachedChatListSearchResult?
if let searchQuery = item.interaction.searchTextHighightState {
if let cached = currentChatListSearchResult, cached.matches(text: item.message.text, searchQuery: searchQuery) {
if let searchQuery = item.interaction.searchTextHighightState, let message = item.message {
if let cached = currentChatListSearchResult, cached.matches(text: message.text, searchQuery: searchQuery) {
chatListSearchResult = cached
} else {
let (ranges, text) = findSubstringRanges(in: item.message.text, query: searchQuery)
let (ranges, text) = findSubstringRanges(in: message.text, query: searchQuery)
chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges)
}
} else {
@ -469,8 +472,8 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
var descriptionMaxNumberOfLines = 3
if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first {
var text = NSMutableAttributedString(string: item.message.text, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first, let message = item.message {
var text = NSMutableAttributedString(string: message.text, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
for range in chatListSearchResult.resultRanges {
let stringRange = NSRange(range, in: chatListSearchResult.text)
if stringRange.location >= 0 && stringRange.location + stringRange.length <= text.length {
@ -496,7 +499,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -536,8 +539,8 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
var authorString = ""
if item.isGlobalSearchResult {
authorString = stringForFullAuthorName(message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if item.isGlobalSearchResult, let message = item.message {
authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
}
let authorText = NSAttributedString(string: authorString, font: authorFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
@ -716,7 +719,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
override public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil {
if let item = self.item, item.message?.id == id, self.iconImageNode.supernode != nil {
let iconImageNode = self.iconImageNode
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
return (iconImageNode?.view.snapshotContentTree(unhide: true), nil)
@ -726,7 +729,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
override public func updateHiddenMedia() {
if let interaction = self.interaction, let item = self.item, interaction.getHiddenMedia()[item.message.id] != nil {
if let interaction = self.interaction, let item = self.item, let message = item.message, interaction.getHiddenMedia()[message.id] != nil {
self.iconImageNode.isHidden = true
} else {
self.iconImageNode.isHidden = false
@ -737,18 +740,18 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
func activateMedia() {
if let item = self.item, let currentPrimaryUrl = self.currentPrimaryUrl {
if let item = self.item, let message = item.message, let currentPrimaryUrl = self.currentPrimaryUrl {
if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if content.instantPage != nil {
if websiteType(of: content.websiteName) == .instagram {
if !item.interaction.openMessage(item.message, .default) {
item.interaction.openInstantPage(item.message, nil)
if !item.interaction.openMessage(message, .default) {
item.interaction.openInstantPage(message, nil)
}
} else {
item.interaction.openInstantPage(item.message, nil)
item.interaction.openInstantPage(message, nil)
}
} else {
if isTelegramMeLink(content.url) || !item.interaction.openMessage(item.message, .link) {
if isTelegramMeLink(content.url) || !item.interaction.openMessage(message, .link) {
item.interaction.openUrl(currentPrimaryUrl, false, false, nil)
}
}
@ -801,11 +804,11 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap, .longTap:
if let item = self.item, let url = self.urlAtPoint(location) {
if let item = self.item, let message = item.message, let url = self.urlAtPoint(location) {
if case .longTap = gesture {
item.interaction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message)
item.interaction.longTap(ChatControllerInteractionLongTapAction.url(url), message)
} else if url == self.currentPrimaryUrl {
if !item.interaction.openMessage(item.message, .default) {
if !item.interaction.openMessage(message, .default) {
item.interaction.openUrl(url, false, false, nil)
}
} else {
@ -824,7 +827,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
if let item = self.item, let message = item.message {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.linkNode.frame
@ -846,7 +849,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
self.linkHighlightingNode = linkHighlightingNode
self.offsetContainerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode)
}

View file

@ -293,6 +293,9 @@ public final class LocationPickerController: ViewController, AttachmentContainab
self.displayNode = LocationPickerControllerNode(context: self.context, presentationData: self.presentationData, mode: self.mode, interaction: interaction, locationManager: self.locationManager)
self.displayNodeDidLoad()
self.controllerNode.beganInteractiveDragging = { [weak self] in
self?.requestAttachmentMenuExpansion()
}
self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send))
|> deliverOnMainQueue).start(next: { [weak self] next in
@ -333,4 +336,10 @@ public final class LocationPickerController: ViewController, AttachmentContainab
self.interaction?.openSearch()
}
public func prepareForReuse() {
self.interaction?.updateMapMode(.map)
self.interaction?.dismissSearch()
self.scrollToTop?()
}
}

View file

@ -269,6 +269,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
private var listOffset: CGFloat?
var beganInteractiveDragging: () -> Void = {}
init(context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, interaction: LocationPickerInteraction, locationManager: LocationManager) {
self.context = context
self.presentationData = presentationData
@ -654,6 +656,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
guard let strongSelf = self else {
return
}
strongSelf.beganInteractiveDragging()
strongSelf.updateState { state in
var state = state
state.displayingMapModeOptions = false

View file

@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MediaPickerUI",
module_name = "MediaPickerUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/AppBundle:AppBundle",
"//submodules/CheckNode:CheckNode",
"//submodules/MergeLists:MergeLists",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyUI:LegacyUI",
"//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/PhotoResources:PhotoResources",
"//submodules/ContextUI:ContextUI",
"//submodules/MosaicLayout:MosaicLayout",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebSearchUI:WebSearchUI",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,64 @@
import Foundation
import UIKit
import Photos
import SwiftSignalKit
private let imageManager = PHCachingImageManager()
private let assetsQueue = Queue()
func assetImage(fetchResult: PHFetchResult<PHAsset>, index: Int, targetSize: CGSize, exact: Bool) -> Signal<UIImage?, NoError> {
let asset = fetchResult[index]
return assetImage(asset: asset, targetSize: targetSize, exact: exact)
}
func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool) -> Signal<UIImage?, NoError> {
return Signal { subscriber in
let options = PHImageRequestOptions()
if exact {
options.resizeMode = .exact
}
let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
var degraded = false
if let info = info {
if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled {
return
}
if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue {
degraded = true
}
}
if let image = image {
subscriber.putNext(image)
if !degraded {
subscriber.putCompletion()
}
}
}
return ActionDisposable {
assetsQueue.async {
imageManager.cancelImageRequest(token)
}
}
} |> runOn(assetsQueue)
}
func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAsset?, NoError> {
return Signal { subscriber in
let asset = fetchResult[index]
let options = PHVideoRequestOptions()
let token = imageManager.requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
if let avAsset = avAsset {
subscriber.putNext(avAsset)
subscriber.putCompletion()
}
}
return ActionDisposable {
imageManager.cancelImageRequest(token)
}
}
}

View file

@ -0,0 +1,284 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import Postbox
import SSignalKit
import TelegramPresentationData
import AccountContext
import LegacyComponents
import LegacyUI
import LegacyMediaPickerUI
import Photos
private func galleryFetchResultItems(fetchResult: PHFetchResult<PHAsset>, index: Int, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, stickersContext: TGPhotoPaintStickersContext, immediateThumbnail: UIImage?) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
var focusItem: TGModernGalleryItem?
var galleryItems: [TGModernGalleryItem] = []
let legacyFetchResult = TGMediaAssetFetchResult(phFetchResult: fetchResult as? PHFetchResult<AnyObject>, reversed: true)
for i in 0 ..< fetchResult.count {
if let galleryItem = TGMediaPickerGalleryFetchResultItem(fetchResult: legacyFetchResult, index: UInt(i)) {
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
galleryItem.stickersContext = stickersContext
galleryItems.append(galleryItem)
if i == index {
galleryItem.immediateThumbnailImage = immediateThumbnail
focusItem = galleryItem
}
}
}
return (galleryItems, focusItem)
}
private func gallerySelectionItems(item: TGMediaSelectableItem, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, stickersContext: TGPhotoPaintStickersContext, immediateThumbnail: UIImage?) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
var focusItem: TGModernGalleryItem?
var galleryItems: [TGModernGalleryItem] = []
if let selectionContext = selectionContext {
for case let selectedItem as TGMediaSelectableItem in selectionContext.selectedItems() {
if let asset = selectedItem as? TGMediaAsset {
let galleryItem: (TGModernGallerySelectableItem & TGModernGalleryEditableItem)
switch asset.type {
case TGMediaAssetVideoType:
galleryItem = TGMediaPickerGalleryVideoItem(asset: asset)
case TGMediaAssetGifType:
let convertedAsset = TGCameraCapturedVideo(asset: asset, livePhoto: false)
galleryItem = TGMediaPickerGalleryVideoItem(asset: convertedAsset)
default:
galleryItem = TGMediaPickerGalleryPhotoItem(asset: asset)
}
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
galleryItem.stickersContext = stickersContext
galleryItems.append(galleryItem)
if selectedItem.uniqueIdentifier == item.uniqueIdentifier {
if let galleryItem = galleryItem as? TGMediaPickerGalleryItem {
galleryItem.immediateThumbnailImage = immediateThumbnail
}
focusItem = galleryItem
}
}
}
}
return (galleryItems, focusItem)
}
enum LegacyMediaPickerGallerySource {
case fetchResult(fetchResult: PHFetchResult<PHAsset>, index: Int)
case selection(item: TGMediaSelectableItem)
}
func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) {
let reminder = peer?.id == context.account.peerId
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
paintStickersContext.presentStickersController = { completion in
if let presentStickers = presentStickers {
return presentStickers({ file, animated, view, rect in
let coder = PostboxEncoder()
coder.encodeRootObject(file)
completion?(coder.makeData(), animated, view, rect)
})
} else {
return nil
}
}
let controller = TGModernGalleryController(context: legacyController.context)!
controller.asyncTransitionIn = true
legacyController.bind(controller: controller)
let (items, focusItem): ([TGModernGalleryItem], TGModernGalleryItem?)
switch source {
case let .fetchResult(fetchResult, index):
(items, focusItem) = galleryFetchResultItems(fetchResult: fetchResult, index: index, selectionContext: selectionContext, editingContext: editingContext, stickersContext: paintStickersContext, immediateThumbnail: immediateThumbnail)
case let .selection(item):
(items, focusItem) = gallerySelectionItems(item: item, selectionContext: selectionContext, editingContext: editingContext, stickersContext: paintStickersContext, immediateThumbnail: immediateThumbnail)
}
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))!
model.stickersContext = paintStickersContext
controller.model = model
model.controller = controller
model.willFinishEditingItem = { item, adjustments, representation, hasChanges in
if hasChanges {
editingContext.setAdjustments(adjustments, for: item)
editingContext.setTemporaryRep(representation, for: item)
}
if let selectionContext = selectionContext, adjustments != nil, let item = item as? TGMediaSelectableItem {
selectionContext.setItem(item, selected: true)
}
}
model.didFinishEditingItem = { item, adjustments, result, thumbnail in
editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: false)
}
model.saveItemCaption = { item, caption in
editingContext.setCaption(caption, for: item)
if let selectionContext = selectionContext, let caption = caption, caption.length > 0, let item = item as? TGMediaSelectableItem {
selectionContext.setItem(item, selected: true)
}
}
model.didFinishRenderingFullSizeImage = { item, result in
editingContext.setFullSizeImage(result, for: item)
}
model.requestAdjustments = { item in
return editingContext.adjustments(for: item)
}
if let selectionContext = selectionContext {
model.interfaceView.updateSelectionInterface(selectionContext.count(), counterVisible: selectionContext.count() > 0, animated: false)
}
controller.transitionHost = {
return transitionHostView()
}
var transitionedIn = false
controller.itemFocused = { item in
if let item = item as? TGMediaPickerGalleryItem, transitionedIn {
updateHiddenMedia(item.asset.uniqueIdentifier)
}
}
controller.beginTransitionIn = { item, itemView in
if let item = item as? TGMediaPickerGalleryItem {
if let itemView = itemView as? TGMediaPickerGalleryVideoItemView {
itemView.setIsCurrent(true)
}
return transitionView(item.asset.uniqueIdentifier)
} else {
return nil
}
}
controller.startedTransitionIn = {
transitionedIn = true
if let focusItem = focusItem as? TGModernGallerySelectableItem {
updateHiddenMedia(focusItem.selectableMediaItem().uniqueIdentifier)
}
}
controller.beginTransitionOut = { item, itemView in
if let item = item as? TGMediaPickerGalleryItem {
if let itemView = itemView as? TGMediaPickerGalleryVideoItemView {
itemView.stop()
}
return transitionView(item.asset.uniqueIdentifier)
} else {
return nil
}
}
controller.finishedTransitionIn = { [weak model] _, _ in
model?.interfaceView.setSelectedItemsModel(model?.selectedItemsModel)
}
controller.completedTransitionOut = { [weak legacyController] in
updateHiddenMedia(nil)
legacyController?.dismiss()
}
model.interfaceView.donePressed = { [weak controller] item in
if let item = item as? TGMediaPickerGalleryItem {
controller?.dismissWhenReady(animated: true)
completed(item.asset, false, nil)
}
}
model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in
if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext {
var effectiveHasSchedule = hasSchedule
if let editingContext = editingContext {
for item in selectionContext.selectedItems() {
if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 {
effectiveHasSchedule = false
break
}
}
}
let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
let controller = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer)
let dismissImpl = { [weak model] in
model?.dismiss(true, false)
}
controller.send = {
dismissImpl()
completed(item.asset, false, nil)
}
controller.sendSilently = {
dismissImpl()
completed(item.asset, true, nil)
}
controller.schedule = {
presentSchedulePicker(true, { time in
dismissImpl()
completed(item.asset, false, time)
})
}
controller.sendWithTimer = {
presentTimerPicker { time in
dismissImpl()
var items = selectionContext.selectedItems() ?? []
items.append(item.asset as Any)
for case let item as TGMediaEditableItem in items {
editingContext?.setTimer(time as NSNumber, for: item)
}
completed(item.asset, false, nil)
}
}
controller.customDismissBlock = { [weak legacySheetController] in
legacySheetController?.dismiss()
}
legacySheetController.bind(controller: controller)
present(legacySheetController, nil)
}
}
model.interfaceView.setThumbnailSignalForItem { item in
let imageSignal = SSignal(generator: { subscriber in
var asset: PHAsset?
if let item = item as? TGCameraCapturedVideo {
asset = item.originalAsset.backingAsset
} else if let item = item as? TGMediaAsset {
asset = item.backingAsset
}
var disposable: Disposable?
if let asset = asset {
let scale = min(2.0, UIScreenScale)
disposable = assetImage(asset: asset, targetSize: CGSize(width: 128.0 * scale, height: 128.0 * scale), exact: false).start(next: { image in
subscriber.putNext(image)
}, completed: {
subscriber.putCompletion()
})
} else {
subscriber.putCompletion()
}
return SBlockDisposable(block: {
disposable?.dispose()
})
})
if let item = item as? TGMediaEditableItem {
return editingContext.thumbnailImageSignal(for: item).map(toSignal: { result in
if let result = result {
return SSignal.single(result)
} else {
return imageSignal
}
})
} else {
return imageSignal
}
}
present(legacyController, nil)
}

View file

@ -0,0 +1,101 @@
import Foundation
import UIKit
import SwiftSignalKit
import Photos
import AVFoundation
class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
private var registeredChangeObserver = false
private let changeSink = ValuePipe<PHChange>()
private let mediaAccessSink = ValuePipe<PHAuthorizationStatus>()
private let cameraAccessSink = ValuePipe<AVAuthorizationStatus?>()
override init() {
super.init()
if PHPhotoLibrary.authorizationStatus() == .authorized {
PHPhotoLibrary.shared().register(self)
self.registeredChangeObserver = true
}
}
deinit {
if self.registeredChangeObserver {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
self.changeSink.putNext(changeInstance)
}
func fetchResultAssets(_ initialFetchResult: PHFetchResult<PHAsset>) -> Signal<PHFetchResult<PHAsset>?, NoError> {
let fetchResult = Atomic<PHFetchResult<PHAsset>>(value: initialFetchResult)
return .single(initialFetchResult)
|> then(
self.changeSink.signal()
|> mapToSignal { change in
if let updatedFetchResult = change.changeDetails(for: fetchResult.with { $0 })?.fetchResultAfterChanges {
let _ = fetchResult.modify { _ in return updatedFetchResult }
return .single(updatedFetchResult)
} else {
return .complete()
}
}
)
}
func recentAssets() -> Signal<PHFetchResult<PHAsset>?, NoError> {
let collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
if let collection = collections.firstObject {
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: nil)
return fetchResultAssets(initialFetchResult)
} else {
return .single(nil)
}
}
func mediaAccess() -> Signal<PHAuthorizationStatus, NoError> {
let initialStatus: PHAuthorizationStatus
if #available(iOS 14.0, *) {
initialStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
} else {
initialStatus = PHPhotoLibrary.authorizationStatus()
}
return .single(initialStatus)
|> then(
self.mediaAccessSink.signal()
)
}
func requestMediaAccess() -> Void {
PHPhotoLibrary.requestAuthorization { [weak self] status in
self?.mediaAccessSink.putNext(status)
}
}
func cameraAccess() -> Signal<AVAuthorizationStatus?, NoError> {
#if targetEnvironment(simulator)
return .single(.authorized)
#else
if UIImagePickerController.isSourceTypeAvailable(.camera) {
return .single(AVCaptureDevice.authorizationStatus(for: .video))
|> then(
self.cameraAccessSink.signal()
)
} else {
return .single(nil)
}
#endif
}
func requestCameraAccess() -> Void {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] result in
if result {
self?.cameraAccessSink.putNext(.authorized)
} else {
self?.cameraAccessSink.putNext(.denied)
}
})
}
}

View file

@ -0,0 +1,8 @@
//
// MediaGroupsScreen.swift
// _idx_TelegramUI_F082088E_ios_min9.0
//
// Created by Ilya Laktyushin on 22.02.2022.
//
import Foundation

View file

@ -0,0 +1,264 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import AccountContext
import TelegramPresentationData
import TelegramStringFormatting
import Photos
import CheckNode
import LegacyComponents
import PhotoResources
enum MediaPickerGridItemContent: Equatable {
case asset(PHFetchResult<PHAsset>, Int)
}
final class MediaPickerGridItem: GridItem {
let content: MediaPickerGridItemContent
let interaction: MediaPickerInteraction
let theme: PresentationTheme
let section: GridSection? = nil
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme) {
self.content = content
self.interaction = interaction
self.theme = theme
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
switch self.content {
case let .asset(fetchResult, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme)
return node
}
}
func update(node: GridItemNode) {
switch self.content {
case let .asset(fetchResult, index):
guard let node = node as? MediaPickerGridItemNode else {
assertionFailure()
return
}
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme)
}
}
}
private let maskImage = generateImage(CGSize(width: 1.0, height: 24.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.6).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
final class MediaPickerGridItemNode: GridItemNode {
var currentState: (PHFetchResult<PHAsset>, Int)?
private let imageNode: ImageNode
private var checkNode: InteractiveCheckNode?
private let gradientNode: ASImageNode
private let typeIconNode: ASImageNode
private let durationNode: ImmediateTextNode
private var interaction: MediaPickerInteraction?
private var theme: PresentationTheme?
private var currentIsPreviewing = false
var selected: (() -> Void)?
override init() {
self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.contentMode = .scaleAspectFill
self.imageNode.isLayerBacked = false
self.gradientNode = ASImageNode()
self.gradientNode.displaysAsynchronously = false
self.gradientNode.displayWithoutProcessing = true
self.gradientNode.image = maskImage
self.typeIconNode = ASImageNode()
self.typeIconNode.displaysAsynchronously = false
self.typeIconNode.displayWithoutProcessing = true
self.durationNode = ImmediateTextNode()
super.init()
self.addSubnode(self.imageNode)
}
var identifier: String {
return self.asset?.localIdentifier ?? ""
}
private var asset: PHAsset? {
if let (fetchResult, index) = self.currentState {
return fetchResult[index]
} else {
return nil
}
}
func updateSelectionState() {
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)
}
}
}
self.addSubnode(checkNode)
self.checkNode = checkNode
self.setNeedsLayout()
}
if let asset = self.asset, let interaction = self.interaction, let selectionState = interaction.selectionState {
let selected = selectionState.isIdentifierSelected(asset.localIdentifier)
if let legacyAsset = TGMediaAsset(phAsset: asset) {
let index = selectionState.index(of: legacyAsset)
if index != NSNotFound {
self.checkNode?.content = .counter(Int(index))
}
}
self.checkNode?.setSelected(selected, animated: false)
}
}
func updateHiddenMedia() {
if let asset = self.asset {
let wasHidden = self.isHidden
self.isHidden = self.interaction?.hiddenMediaId == asset.localIdentifier
if !self.isHidden && wasHidden {
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme) {
self.interaction = interaction
self.theme = theme
if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index {
let editingContext = interaction.editingState
let asset = fetchResult.object(at: index)
let editedSignal = Signal<UIImage?, NoError> { subscriber in
if let signal = editingContext.thumbnailImageSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
if let image = next as? UIImage {
subscriber.putNext(image)
} else {
subscriber.putNext(nil)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let scale = min(2.0, UIScreenScale)
let targetSize = CGSize(width: 140.0 * scale, height: 140.0 * scale)
let originalSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|> mapToSignal { result in
if let result = result {
return .single(result)
} else {
return originalSignal
}
}
self.imageNode.setSignal(imageSignal)
if asset.mediaType == .video {
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo")
} else if asset.mediaSubtypes.contains(.videoTimelapse) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaTimelapse")
} else {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
}
if self.typeIconNode.supernode == nil {
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.duration)), font: Font.semibold(12.0), textColor: .white)
self.addSubnode(self.gradientNode)
self.addSubnode(self.typeIconNode)
self.addSubnode(self.durationNode)
self.setNeedsLayout()
}
} else {
if self.typeIconNode.supernode != nil {
self.gradientNode.removeFromSupernode()
self.typeIconNode.removeFromSupernode()
self.durationNode.removeFromSupernode()
}
}
self.currentState = (fetchResult, index)
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
override func layout() {
super.layout()
self.imageNode.frame = self.bounds
self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 24.0, width: self.bounds.width, height: 24.0)
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
if self.durationNode.supernode != nil {
let durationSize = self.durationNode.updateLayout(self.bounds.size)
self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 7.0, y: self.bounds.height - durationSize.height - 5.0), size: durationSize)
}
let checkSize = CGSize(width: 29.0, height: 29.0)
self.checkNode?.frame = CGRect(origin: CGPoint(x: self.bounds.width - checkSize.width - 3.0, y: 3.0), size: checkSize)
}
func transitionView() -> UIView {
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
view.frame = self.convert(self.bounds, to: nil)
return view
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
guard let (fetchResult, index) = self.currentState else {
return
}
self.interaction?.openMedia(fetchResult, index, self.imageNode.image)
}
}

View file

@ -0,0 +1,74 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import SolidRoundedButtonNode
final class MediaPickerManageNode: ASDisplayNode {
enum Subject {
case limitedMedia
case camera
}
private let textNode: ImmediateTextNode
private let measureButtonNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
var pressed: () -> Void = {}
override init() {
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.textAlignment = .left
self.textNode.maximumNumberOfLines = 0
self.measureButtonNode = ImmediateTextNode()
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), fontSize: 15.0, height: 28.0, cornerRadius: 14.0)
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.buttonNode)
self.buttonNode.pressed = { [weak self] in
self?.pressed()
}
}
private var theme: PresentationTheme?
func update(layout: ContainerViewLayout, theme: PresentationTheme, strings: PresentationStrings, subject: Subject, transition: ContainedViewLayoutTransition) -> CGFloat {
let themeUpdated = self.theme != theme
self.theme = theme
let text: String
switch subject {
case .limitedMedia:
text = strings.Attachment_LimitedMediaAccessText
case .camera:
text = strings.Attachment_CameraAccessText
}
let title = strings.Attachment_Manage.uppercased()
self.measureButtonNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .white, paragraphAlignment: .center)
let measureButtonSize = self.measureButtonNode.updateLayout(layout.size)
let buttonWidth = measureButtonSize.width + 26.0
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .left)
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 16.0 - buttonWidth - 26.0, height: layout.size.height))
let panelHeight = max(64.0, textSize.height + 10.0)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: floorToScreenPixels((panelHeight - textSize.height) / 2.0) - 5.0), size: textSize))
if themeUpdated {
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
}
self.buttonNode.title = title
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - buttonWidth - 10.0, y: floorToScreenPixels((panelHeight - buttonHeight) / 2.0) - 5.0), size: CGSize(width: buttonWidth, height: buttonHeight)))
return panelHeight
}
}

View file

@ -0,0 +1,131 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ManagedAnimationNode
import ContextUI
final class MediaPickerMoreButtonNode: ASDisplayNode {
class MoreIconNode: ManagedAnimationNode {
enum State: Equatable {
case more
case search
}
private let duration: Double = 0.21
var iconState: State = .search
init() {
super.init(size: CGSize(width: 30.0, height: 30.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0))
}
func play() {
if case .more = self.iconState {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
}
}
func enqueueState(_ state: State, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
let source = ManagedAnimationSource.local("anim_moretosearch")
let totalLength: Int = 90
if animated {
switch previousState {
case .more:
switch state {
case .more:
break
case .search:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
}
case .search:
switch state {
case .more:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
case .search:
break
}
}
} else {
switch state {
case .more:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
case .search:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0))
}
}
}
}
var action: ((ASDisplayNode, ContextGesture?) -> Void)?
private let containerNode: ContextControllerSourceNode
let contextSourceNode: ContextReferenceContentNode
private let buttonNode: HighlightableButtonNode
let iconNode: MoreIconNode
var theme: PresentationTheme {
didSet {
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
}
}
init(theme: PresentationTheme) {
self.theme = theme
self.contextSourceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.buttonNode = HighlightableButtonNode()
self.iconNode = MoreIconNode()
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
super.init()
self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.contextSourceNode.addSubnode(self.iconNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.action?(strongSelf.contextSourceNode, gesture)
}
}
@objc private func buttonPressed() {
self.action?(self.contextSourceNode, nil)
if case .more = self.iconNode.iconState {
self.iconNode.play()
}
}
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)
let size = CGSize(width: animationSize.width + inset * 2.0, height: constrainedSize.height)
let bounds = CGRect(origin: CGPoint(), size: size)
self.buttonNode.frame = bounds
self.containerNode.frame = bounds
self.contextSourceNode.frame = bounds
return size
}
}

View file

@ -0,0 +1,150 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
import PresentationDataUtils
final class MediaPickerPlaceholderNode: ASDisplayNode {
private var animationNode: AnimatedStickerNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
private var validLayout: ContainerViewLayout?
private var cameraButtonNode: HighlightTrackingButtonNode
private var cameraTextNode: ImmediateTextNode
private var cameraIconNode: ASImageNode
var settingsPressed: () -> Void = {}
var cameraPressed: () -> Void = {}
override init() {
self.animationNode = AnimatedStickerNode()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Files"), width: 320, height: 320, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.textAlignment = .center
self.titleNode.maximumNumberOfLines = 1
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 0
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 12.0, gloss: true)
self.cameraButtonNode = HighlightTrackingButtonNode()
self.cameraButtonNode.alpha = 0.0
self.cameraButtonNode.isUserInteractionEnabled = false
self.cameraTextNode = ImmediateTextNode()
self.cameraTextNode.isUserInteractionEnabled = false
self.cameraIconNode = ASImageNode()
self.cameraIconNode.displaysAsynchronously = false
self.cameraIconNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.cameraButtonNode)
self.cameraButtonNode.addSubnode(self.cameraTextNode)
self.cameraButtonNode.addSubnode(self.cameraIconNode)
self.cameraButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.cameraTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.cameraTextNode.alpha = 0.4
strongSelf.cameraIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.cameraIconNode.alpha = 0.4
} else {
strongSelf.cameraTextNode.alpha = 1.0
strongSelf.cameraTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.cameraIconNode.alpha = 1.0
strongSelf.cameraIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.cameraButtonNode.addTarget(self, action: #selector(self.cameraButtonPressed), forControlEvents: .touchUpInside)
self.buttonNode.pressed = { [weak self] in
self?.settingsPressed()
}
}
@objc private func cameraButtonPressed() {
self.cameraPressed()
}
private var theme: PresentationTheme?
func update(layout: ContainerViewLayout, theme: PresentationTheme, strings: PresentationStrings, hasCamera: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
let themeUpdated = self.theme != theme
self.theme = theme
var insets = layout.insets(options: [])
insets.top += -160.0
let imageSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 16.0
let buttonSpacing: CGFloat = 15.0
let cameraSpacing: CGFloat = 13.0
let imageSize = CGSize(width: 144.0, height: 144.0)
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
self.animationNode.updateLayout(size: imageSize)
if themeUpdated {
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
self.cameraIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/OpenCamera"), color: theme.list.itemAccentColor)
}
self.buttonNode.title = strings.Attachment_OpenSettings
let buttonWidth: CGFloat = 248.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
self.titleNode.attributedText = NSAttributedString(string: strings.Attachment_MediaAccessTitle, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: strings.Attachment_MediaAccessText, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.cameraTextNode.attributedText = NSAttributedString(string: strings.Attachment_OpenCamera, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let cameraSize = self.cameraTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height + buttonSpacing + buttonHeight + cameraSpacing + cameraSize.height
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - buttonWidth - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.textNode.frame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight)))
if let image = self.cameraIconNode.image {
let cameraTotalSize = CGSize(width: cameraSize.width + image.size.width + 10.0, height: 44.0)
transition.updateFrame(node: self.cameraIconNode, frame: CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((cameraTotalSize.height - image.size.height) / 2.0)), size: image.size))
transition.updateFrame(node: self.cameraTextNode, frame: CGRect(origin: CGPoint(x: cameraTotalSize.width - cameraSize.width, y: floorToScreenPixels((cameraTotalSize.height - cameraSize.height) / 2.0)), size: cameraSize))
transition.updateFrame(node: self.cameraButtonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - cameraTotalSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.buttonNode.frame.maxY + cameraSpacing), size: cameraTotalSize))
}
self.cameraButtonNode.isUserInteractionEnabled = hasCamera
transition.updateAlpha(node: self.cameraButtonNode, alpha: hasCamera ? 1.0 : 0.0)
}
}

View file

@ -0,0 +1,950 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import Photos
import PhotosUI
import LegacyComponents
import LegacyMediaPickerUI
import AttachmentUI
import ContextUI
import WebSearchUI
final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
let toggleSelection: (TGMediaSelectableItem, Bool) -> Void
let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void
let schedule: () -> Void
let dismissInput: () -> Void
let selectionState: TGMediaSelectionContext?
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) {
self.openMedia = openMedia
self.openSelectedMedia = openSelectedMedia
self.toggleSelection = toggleSelection
self.sendSelected = sendSelected
self.schedule = schedule
self.dismissInput = dismissInput
self.selectionState = selectionState
self.editingState = editingState
}
}
private struct MediaPickerGridEntry: Comparable, Identifiable {
let stableId: Int
let content: MediaPickerGridItemContent
static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(account: Account, interaction: MediaPickerInteraction, theme: PresentationTheme) -> MediaPickerGridItem {
return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme)
}
}
private struct MediaPickerGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let scrollToItem: GridNodeScrollToItem?
init(previousList: [MediaPickerGridEntry], list: [MediaPickerGridEntry], account: Account, interaction: MediaPickerInteraction, theme: PresentationTheme, scrollToItem: GridNodeScrollToItem?) {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
self.deletions = deleteIndices
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) }
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) }
self.scrollToItem = scrollToItem
}
}
public final class MediaPickerScreen: ViewController, AttachmentContainable {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let peer: EnginePeer?
private let chatLocation: ChatLocation?
private let titleView: MediaPickerTitleView
private let moreButtonNode: MediaPickerMoreButtonNode
public weak var webSearchController: WebSearchController?
public var openCamera: ((TGAttachmentCameraView?) -> Void)?
public var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?
public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in }
public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in }
public var presentWebSearch: () -> Void = {}
public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?) -> Void = { _, _, _ in }
public var requestAttachmentMenuExpansion: () -> Void = {}
private class Node: ViewControllerTracingNode {
enum DisplayMode {
case all
case selected
}
enum State {
case noAccess(cameraAccess: AVAuthorizationStatus?)
case assets(fetchResult: PHFetchResult<PHAsset>?, mediaAccess: PHAuthorizationStatus, cameraAccess: AVAuthorizationStatus?)
}
private weak var controller: MediaPickerScreen?
fileprivate var interaction: MediaPickerInteraction?
private var presentationData: PresentationData
private let mediaAssetsContext: MediaAssetsContext
private let backgroundNode: NavigationBackgroundNode
private let gridNode: GridNode
private var cameraView: TGAttachmentCameraView?
private var placeholderNode: MediaPickerPlaceholderNode?
private var manageNode: MediaPickerManageNode?
private var selectionNode: MediaPickerSelectedListNode?
private var nextStableId: Int = 1
private var currentEntries: [MediaPickerGridEntry] = []
private var enqueuedTransactions: [MediaPickerGridTransaction] = []
private var state: State?
private var itemsDisposable: Disposable?
private var selectionChangedDisposable: Disposable?
private var itemsDimensionsUpdatedDisposable: Disposable?
private var hiddenMediaDisposable: Disposable?
private let hiddenMediaId = Promise<String?>(nil)
private var didSetReady = false
private let _ready = Promise<Bool>()
var ready: Promise<Bool> {
return self._ready
}
private var validLayout: (ContainerViewLayout, CGFloat)?
init(controller: MediaPickerScreen) {
self.controller = controller
self.presentationData = controller.presentationData
let mediaAssetsContext = MediaAssetsContext()
self.mediaAssetsContext = mediaAssetsContext
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.gridNode = GridNode()
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.backgroundNode)
self.addSubnode(self.gridNode)
self.interaction = MediaPickerInteraction(openMedia: { [weak self] fetchResult, index, immediateThumbnail in
self?.openMedia(fetchResult: fetchResult, index: index, immediateThumbnail: immediateThumbnail)
}, openSelectedMedia: { [weak self] item, immediateThumbnail in
self?.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail)
}, toggleSelection: { [weak self] item, value in
if let strongSelf = self {
strongSelf.interaction?.selectionState?.setItem(item, selected: value)
}
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState {
if let currentItem = currentItem {
selectionState.setItem(currentItem, selected: true)
}
strongSelf.send(silently: silently, scheduleTime: scheduleTime, animated: animated)
}
}, schedule: { [weak self] in
if let strongSelf = self {
strongSelf.controller?.presentSchedulePicker(false, { [weak self] time in
self?.interaction?.sendSelected(nil, false, time, true)
})
}
}, dismissInput: { [weak self] in
if let strongSelf = self {
strongSelf.dismissInput()
}
}, selectionState: TGMediaSelectionContext(), editingState: TGMediaEditingContext())
self.interaction?.selectionState?.grouping = true
let updatedState = combineLatest(mediaAssetsContext.mediaAccess(), mediaAssetsContext.cameraAccess())
|> mapToSignal { mediaAccess, cameraAccess -> Signal<State, NoError> in
if case .notDetermined = mediaAccess {
return .single(.assets(fetchResult: nil, mediaAccess: mediaAccess, cameraAccess: cameraAccess))
} else if [.restricted, .denied].contains(mediaAccess) {
return .single(.noAccess(cameraAccess: cameraAccess))
} else {
return mediaAssetsContext.recentAssets()
|> map { fetchResult in
return .assets(fetchResult: fetchResult, mediaAccess: mediaAccess, cameraAccess: cameraAccess)
}
}
}
self.itemsDisposable = (updatedState
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.updateState(state)
})
self.gridNode.scrollingInitiated = { [weak self] in
self?.dismissInput()
}
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|> deliverOnMainQueue).start(next: { [weak self] id in
if let strongSelf = self {
strongSelf.interaction?.hiddenMediaId = id
strongSelf.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode {
itemNode.updateHiddenMedia()
}
}
strongSelf.selectionNode?.updateHiddenMedia()
}
})
if let selectionState = self.interaction?.selectionState {
func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal<Void, NoError> {
return Signal { subscriber in
let disposable = selectionState.selectionChangedSignal()?.start(next: { next in
subscriber.putNext(Void())
}, completed: {})
return ActionDisposable {
disposable?.dispose()
}
}
}
self.selectionChangedDisposable = (selectionChangedSignal(selectionState: selectionState)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.updateSelectionState()
}
})
}
if let editingState = self.interaction?.editingState {
func itemsDimensionsUpdatedSignal(editingState: TGMediaEditingContext) -> Signal<Void, NoError> {
return Signal { subscriber in
let disposable = editingState.cropAdjustmentsUpdatedSignal()?.start(next: { next in
subscriber.putNext(Void())
}, completed: {})
return ActionDisposable {
disposable?.dispose()
}
}
}
self.itemsDimensionsUpdatedDisposable = (itemsDimensionsUpdatedSignal(editingState: editingState)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.updateSelectionState()
}
})
}
}
deinit {
self.itemsDisposable?.dispose()
self.hiddenMediaDisposable?.dispose()
self.selectionChangedDisposable?.dispose()
self.itemsDimensionsUpdatedDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
let cameraView = TGAttachmentCameraView(forSelfPortrait: false)!
cameraView.clipsToBounds = true
cameraView.removeCorners()
cameraView.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.controller?.openCamera?(strongSelf.cameraView)
}
}
self.cameraView = cameraView
cameraView.startPreview()
self.gridNode.scrollView.addSubview(cameraView)
// self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
}
private func dismissInput() {
self.view.window?.endEditing(true)
}
private var requestedMediaAccess = false
private var requestedCameraAccess = false
private func updateState(_ state: State) {
guard let interaction = self.interaction, let controller = self.controller else {
return
}
let previousState = self.state
self.state = state
var stableId: Int = 0
var entries: [MediaPickerGridEntry] = []
var updateLayout = false
switch state {
case let .noAccess(cameraAccess):
if case .assets = previousState {
updateLayout = true
} else if case let .noAccess(previousCameraAccess) = previousState, previousCameraAccess != cameraAccess {
updateLayout = true
}
if case .notDetermined = cameraAccess, !self.requestedCameraAccess {
self.requestedCameraAccess = true
self.mediaAssetsContext.requestCameraAccess()
}
case let .assets(fetchResult, mediaAccess, cameraAccess):
if let fetchResult = fetchResult {
for i in 0 ..< fetchResult.count {
entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, fetchResult.count - i - 1)))
stableId += 1
}
if case let .assets(previousFetchResult, _, previousCameraAccess) = previousState, previousFetchResult == nil || previousCameraAccess != cameraAccess {
updateLayout = true
}
if case .notDetermined = cameraAccess, !self.requestedCameraAccess {
self.requestedCameraAccess = true
self.mediaAssetsContext.requestCameraAccess()
}
} else if case .notDetermined = mediaAccess, !self.requestedMediaAccess {
self.requestedMediaAccess = true
self.mediaAssetsContext.requestMediaAccess()
}
}
let previousEntries = self.currentEntries
self.currentEntries = entries
let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, account: controller.context.account, interaction: interaction, theme: self.presentationData.theme, scrollToItem: nil)
self.enqueueTransaction(transaction)
if updateLayout, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
}
}
private func updateSelectionState() {
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode {
itemNode.updateSelectionState()
}
}
self.selectionNode?.updateSelectionState()
let count = Int32(self.interaction?.selectionState?.count() ?? 0)
self.controller?.updateSelectionState(count: count)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
}
private var currentDisplayMode: DisplayMode = .all
func updateMode(_ displayMode: DisplayMode) {
self.currentDisplayMode = displayMode
if case .selected = displayMode, self.selectionNode == nil, let controller = self.controller {
let selectionNode = MediaPickerSelectedListNode(context: controller.context)
selectionNode.alpha = 0.0
selectionNode.isUserInteractionEnabled = false
selectionNode.interaction = self.interaction
self.insertSubnode(selectionNode, aboveSubnode: self.gridNode)
self.selectionNode = selectionNode
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
self.gridNode.isUserInteractionEnabled = displayMode == .all
if let selectionNode = self.selectionNode {
transition.updateAlpha(node: selectionNode, alpha: displayMode == .selected ? 1.0 : 0.0)
selectionNode.isUserInteractionEnabled = displayMode == .selected
}
}
private func openMedia(fetchResult: PHFetchResult<PHAsset>, index: Int, immediateThumbnail: UIImage?) {
guard let controller = self.controller, let interaction = self.interaction, let (layout, _) = self.validLayout else {
return
}
Queue.mainQueue().justDispatch {
self.dismissInput()
}
let index = fetchResult.count - index - 1
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: true, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, initialLayout: layout, transitionHostView: { [weak self] in
return self?.gridNode.view
}, transitionView: { [weak self] identifier in
return self?.transitionView(for: identifier)
}, completed: { [weak self] result, silently, scheduleTime in
if let strongSelf = self {
strongSelf.interaction?.sendSelected(result, silently, scheduleTime, false)
}
}, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
})
}
private func openSelectedMedia(item: TGMediaSelectableItem, immediateThumbnail: UIImage?) {
guard let controller = self.controller, let interaction = self.interaction, let (layout, _) = self.validLayout else {
return
}
Queue.mainQueue().justDispatch {
self.dismissInput()
}
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: true, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, initialLayout: layout, transitionHostView: { [weak self] in
return self?.selectionNode?.view
}, transitionView: { [weak self] identifier in
return self?.transitionView(for: identifier)
}, completed: { [weak self] result, silently, scheduleTime in
if let strongSelf = self {
strongSelf.interaction?.sendSelected(result, silently, scheduleTime, false)
}
}, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
})
}
fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool) {
guard let signals = TGMediaAssetsController.resultSignals(for: self.interaction?.selectionState, editingContext: self.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else {
return
}
self.controller?.legacyCompletion(signals, silently, scheduleTime)
self.controller?.dismiss(animated: animated)
}
private func openLimitedMediaOptions() {
let presentationData = self.presentationData
let controller = ActionSheetController(presentationData: self.presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessSelectMore, color: .accent, action: { [weak self] in
dismissAction()
if #available(iOS 14.0, *), let strongController = self?.controller {
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: strongController)
}
}),
ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessChangeSettings, color: .accent, action: { [weak self] in
dismissAction()
self?.controller?.context.sharedContext.applicationBindings.openSettings()
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.controller?.present(controller, in: .window(.root))
}
private func transitionView(for identifier: String) -> UIView? {
if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 {
return selectionNode.transitionView(for: identifier)
} else {
var transitionNode: MediaPickerGridItemNode?
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier {
transitionNode = itemNode
}
}
return transitionNode?.transitionView()
}
}
private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) {
self.enqueuedTransactions.append(transaction)
if let _ = self.validLayout {
self.dequeueTransaction()
}
}
private func dequeueTransaction() {
if self.enqueuedTransactions.isEmpty {
return
}
let transaction = self.enqueuedTransactions.removeFirst()
self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: transaction.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
}
func scrollToTop(animated: Bool = false) {
if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 {
selectionNode.scrollToTop(animated: animated)
} else {
self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.gridNode.scrollView.contentInset.top), animated: animated)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let firstTime = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [])
insets.top += navigationBarHeight
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
let itemsPerRow: Int
if case .compact = layout.metrics.widthClass {
switch layout.orientation {
case .portrait:
itemsPerRow = 3
case .landscape:
itemsPerRow = 5
}
} else {
itemsPerRow = 3
}
let width = layout.size.width - layout.safeInsets.left - layout.safeInsets.right
let itemSpacing: CGFloat = 1.0
let itemWidth = floorToScreenPixels((width - itemSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
var cameraRect: CGRect? = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: CGSize(width: itemWidth, height: itemWidth * 2.0 + 1.0))
var manageHeight: CGFloat = 0.0
if case let .assets(_, mediaAccess, cameraAccess) = self.state {
if cameraAccess == nil {
cameraRect = nil
}
if case .notDetermined = mediaAccess {
} else {
if case .limited = mediaAccess {
let manageNode: MediaPickerManageNode
if let current = self.manageNode {
manageNode = current
} else {
manageNode = MediaPickerManageNode()
manageNode.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.openLimitedMediaOptions()
}
}
self.manageNode = manageNode
self.gridNode.addSubnode(manageNode)
}
manageHeight = manageNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, subject: .limitedMedia, transition: transition)
transition.updateFrame(node: manageNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -manageHeight), size: CGSize(width: layout.size.width, height: manageHeight)))
} else if [.denied, .restricted].contains(cameraAccess) {
cameraRect = nil
let manageNode: MediaPickerManageNode
if let current = self.manageNode {
manageNode = current
} else {
manageNode = MediaPickerManageNode()
manageNode.pressed = { [weak self] in
self?.controller?.context.sharedContext.applicationBindings.openSettings()
}
self.manageNode = manageNode
self.gridNode.addSubnode(manageNode)
}
manageHeight = manageNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, subject: .camera, transition: transition)
transition.updateFrame(node: manageNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -manageHeight), size: CGSize(width: layout.size.width, height: manageHeight)))
} else if let manageNode = self.manageNode {
self.manageNode = nil
manageNode.removeFromSupernode()
}
}
} else {
cameraRect = nil
}
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: bounds)
transition.updateFrame(node: self.backgroundNode, frame: bounds)
self.backgroundNode.update(size: bounds.size, transition: transition)
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: 200.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady && strongSelf.state != nil {
strongSelf.didSetReady = true
Queue.mainQueue().justDispatch {
strongSelf._ready.set(.single(true))
}
}
})
if let selectionNode = self.selectionNode {
let selectedItems = self.interaction?.selectionState?.selectedItems() as? [TGMediaSelectableItem] ?? []
let updateSelectionNode = {
selectionNode.updateLayout(size: bounds.size, insets: cleanGridInsets, items: selectedItems, grouped: self.controller?.groupedValue ?? true, theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper, bubbleCorners: self.presentationData.chatBubbleCorners, transition: transition)
}
if selectedItems.count < 1 && self.currentDisplayMode == .selected {
self.updateMode(.all)
Queue.mainQueue().after(0.3, updateSelectionNode)
} else {
updateSelectionNode()
}
transition.updateFrame(node: selectionNode, frame: bounds)
}
if let cameraView = self.cameraView {
if let cameraRect = cameraRect {
transition.updateFrame(view: cameraView, frame: cameraRect)
cameraView.isHidden = false
} else {
cameraView.isHidden = true
}
}
if firstTime {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
if case let .noAccess(cameraAccess) = self.state {
var placeholderTransition = transition
let placeholderNode: MediaPickerPlaceholderNode
if let current = self.placeholderNode {
placeholderNode = current
} else {
placeholderNode = MediaPickerPlaceholderNode()
placeholderNode.settingsPressed = { [weak self] in
self?.controller?.context.sharedContext.applicationBindings.openSettings()
}
placeholderNode.cameraPressed = { [weak self] in
self?.controller?.openCamera?(nil)
}
self.insertSubnode(placeholderNode, aboveSubnode: self.gridNode)
self.placeholderNode = placeholderNode
if transition.isAnimated {
placeholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
placeholderTransition = .immediate
}
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
placeholderTransition.updateFrame(node: placeholderNode, frame: bounds)
} else if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
placeholderNode.removeFromSupernode()
}
}
}
private var validLayout: ContainerViewLayout?
private var controllerNode: Node {
return self.displayNode as! Node
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var groupedValue: Bool = true {
didSet {
self.groupedPromise.set(self.groupedValue)
self.controllerNode.interaction?.selectionState?.grouping = self.groupedValue
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .immediate)
}
}
}
private let groupedPromise = ValuePromise<Bool>(true)
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.peer = peer
self.chatLocation = chatLocation
self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0)
self.titleView.title = self.presentationData.strings.Attachment_Gallery
self.moreButtonNode = MediaPickerMoreButtonNode(theme: self.presentationData.theme)
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData, hideBackground: false, hideBadge: false, hideSeparator: true))
self.statusBar.statusBarStyle = .Ignore
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
self.titleView.indexUpdated = { [weak self] index in
if let strongSelf = self {
strongSelf.controllerNode.updateMode(index == 0 ? .all : .selected)
}
}
self.navigationItem.titleView = self.titleView
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self {
strongSelf.searchOrMorePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture)
}
}
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let webSearchController = strongSelf.webSearchController {
webSearchController.scrollToTop?()
} else {
strongSelf.controllerNode.scrollToTop(animated: true)
}
}
}
self.scrollToTopWithTabBar = { [weak self] in
if let strongSelf = self {
if let webSearchController = strongSelf.webSearchController {
webSearchController.cancel()
} else {
strongSelf.scrollToTop?()
}
}
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
self._ready.set(self.controllerNode.ready.get())
super.displayNodeDidLoad()
}
private var selectionCount: Int32 = 0
fileprivate func updateSelectionState(count: Int32) {
self.selectionCount = count
if count > 0 {
self.titleView.segments = [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(count)]
self.titleView.segmentsHidden = false
self.moreButtonNode.iconNode.enqueueState(.more, animated: true)
} else {
self.titleView.segmentsHidden = true
self.moreButtonNode.iconNode.enqueueState(.search, animated: true)
if self.titleView.index != 0 {
Queue.mainQueue().after(0.3) {
self.titleView.index = 0
}
}
}
}
private func updateThemeAndStrings() {
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.titleView.theme = self.presentationData.theme
self.moreButtonNode.theme = self.presentationData.theme
self.controllerNode.updatePresentationData(self.presentationData)
}
@objc private func cancelPressed() {
self.dismiss()
}
public func prepareForReuse() {
if let webSearchController = self.webSearchController {
self.webSearchController = nil
webSearchController.dismiss()
}
self.scrollToTop?()
}
@objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
switch self.moreButtonNode.iconNode.iconState {
case .search:
self.requestAttachmentMenuExpansion()
self.presentWebSearch()
case .more:
let strings = self.presentationData.strings
let selectionCount = self.selectionCount
let items: Signal<ContextController.Items, NoError> = self.groupedPromise.get()
|> deliverOnMainQueue
|> map { [weak self] grouped -> ContextController.Items in
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true)
})))
if selectionCount > 1 {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in
if !grouped {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.groupedValue = true
})))
items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in
if grouped {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.groupedValue = false
})))
}
return ContextController.Items(content: .list(items))
}
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture)
self.presentInGlobalOverlay(contextController)
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
public var mediaPickerContext: AttachmentMediaPickerContext? {
if let interaction = self.controllerNode.interaction {
return MediaPickerContext(interaction: interaction)
} else {
return nil
}
}
}
final class MediaPickerContext: AttachmentMediaPickerContext {
private weak var interaction: MediaPickerInteraction?
var selectionCount: Signal<Int, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
var caption: Signal<NSAttributedString?, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in
if let caption = caption as? NSAttributedString {
subscriber.putNext(caption)
} else {
subscriber.putNext(nil)
}
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
init(interaction: MediaPickerInteraction) {
self.interaction = interaction
}
func setCaption(_ caption: NSAttributedString) {
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
}
func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
self.interaction?.sendSelected(nil, silently, nil, true)
}
func schedule() {
self.interaction?.schedule()
}
}
private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View file

@ -0,0 +1,904 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import CheckNode
import MosaicLayout
import WallpaperBackgroundNode
import AccountContext
private class MediaPickerSelectedItemNode: ASDisplayNode {
let asset: TGMediaAsset
private let interaction: MediaPickerInteraction?
private let imageNode: ImageNode
private var checkNode: InteractiveCheckNode?
private var durationBackgroundNode: ASDisplayNode?
private var durationTextNode: ImmediateTextNode?
private var theme: PresentationTheme?
private var validLayout: CGSize?
var corners: CACornerMask = [] {
didSet {
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
if #available(iOS 11.0, *) {
self.layer.maskedCorners = corners
}
}
}
var radius: CGFloat = 0.0 {
didSet {
self.layer.cornerRadius = radius
}
}
init(asset: TGMediaAsset, interaction: MediaPickerInteraction?) {
self.imageNode = ImageNode()
self.imageNode.contentMode = .scaleAspectFill
self.imageNode.clipsToBounds = true
self.asset = asset
self.interaction = interaction
super.init()
self.clipsToBounds = true
self.addSubnode(self.imageNode)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
}
@objc private func tap() {
self.interaction?.openSelectedMedia(asset, self.imageNode.image)
}
func setup(size: CGSize) {
let editingState = self.interaction?.editingState
let editedSignal = Signal<UIImage?, NoError> { subscriber in
if let editingState = editingState, let signal = editingState.thumbnailImageSignal(forIdentifier: self.asset.uniqueIdentifier) {
let disposable = signal.start(next: { next in
if let image = next as? UIImage {
subscriber.putNext(image)
} else {
subscriber.putNext(nil)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let dimensions: CGSize
if let adjustments = self.interaction?.editingState.adjustments(for: self.asset), adjustments.cropApplied(forAvatar: false) {
dimensions = adjustments.cropRect.size
} else {
dimensions = self.asset.dimensions
}
let scale = min(2.0, UIScreenScale)
let scaledDimensions = dimensions.aspectFilled(CGSize(width: 320.0, height: 320.0))
let targetSize = CGSize(width: scaledDimensions.width * scale, height: scaledDimensions.height * scale)
let originalSignal = assetImage(asset: self.asset.backingAsset, targetSize: targetSize, exact: false)
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|> mapToSignal { result in
if let result = result {
return .single(result)
} else {
return originalSignal
}
}
self.imageNode.setSignal(imageSignal)
}
func updateSelectionState() {
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 interaction = strongSelf.interaction {
interaction.toggleSelection(strongSelf.asset, value)
}
}
self.addSubnode(checkNode)
self.checkNode = checkNode
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
if let interaction = self.interaction, let selectionState = interaction.selectionState, let identifier = self.asset.uniqueIdentifier {
let selected = selectionState.isIdentifierSelected(identifier)
let index = selectionState.index(of: self.asset)
if index != NSNotFound {
self.checkNode?.content = .counter(Int(index))
}
self.checkNode?.setSelected(selected, animated: false)
if let checkNode = self.checkNode {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: checkNode, alpha: selectionState.count() < 2 ? 0.0 : 1.0)
}
}
}
func updateHiddenMedia() {
let wasHidden = self.isHidden
self.isHidden = self.interaction?.hiddenMediaId == asset.uniqueIdentifier
if !self.isHidden && wasHidden {
if let checkNode = self.checkNode, checkNode.alpha > 0.0 {
checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
func update(theme: PresentationTheme) {
var updatedTheme = false
if self.theme != theme {
self.theme = theme
updatedTheme = true
}
if updatedTheme {
self.checkNode?.theme = CheckNodeTheme(theme: theme, style: .overlay)
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
let checkSize = CGSize(width: 29.0, height: 29.0)
if let checkNode = self.checkNode {
transition.updateFrame(node: checkNode, frame: CGRect(origin: CGPoint(x: size.width - checkSize.width - 3.0, y: 3.0), size: checkSize))
}
}
func transitionView() -> UIView {
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
if #available(iOS 13.0, *) {
view.layer.cornerCurve = self.layer.cornerCurve
}
if #available(iOS 11.0, *) {
view.layer.maskedCorners = self.layer.maskedCorners
view.layer.cornerRadius = self.layer.cornerRadius
}
view.frame = self.convert(self.bounds, to: nil)
return view
}
}
final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
fileprivate let wallpaperBackgroundNode: WallpaperBackgroundNode
private let scrollNode: ASScrollNode
private var backgroundNodes: [Int: ASImageNode] = [:]
private var itemNodes: [String: MediaPickerSelectedItemNode] = [:]
private var reorderFeedback: HapticFeedback?
private var reorderNode: ReorderingItemNode?
private var isReordering = false
private var graphics: PrincipalThemeEssentialGraphics?
var interaction: MediaPickerInteraction?
private var validLayout: (size: CGSize, insets: UIEdgeInsets, items: [TGMediaSelectableItem], grouped: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners)?
init(context: AccountContext) {
self.context = context
self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: context.sharedContext.immediateExperimentalUISettings.experimentalBackground)
self.scrollNode = ASScrollNode()
super.init()
self.addSubnode(self.wallpaperBackgroundNode)
self.addSubnode(self.scrollNode)
}
override func didLoad() {
super.didLoad()
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.delegate = self
self.view.addGestureRecognizer(ReorderingGestureRecognizer(shouldBegin: { [weak self] point in
if let strongSelf = self, !strongSelf.scrollNode.view.isTracking {
for (_, itemNode) in strongSelf.itemNodes {
if itemNode.frame.contains(point) {
return (true, true, itemNode)
}
}
return (false, false, nil)
}
return (false, false, nil)
}, willBegin: { _ in
// self?.willBeginReorder(point)
}, began: { [weak self] itemNode in
self?.beginReordering(itemNode: itemNode)
}, ended: { [weak self] point in
self?.endReordering(point: point)
}, moved: { [weak self] offset in
self?.updateReordering(offset: offset)
}))
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.interaction?.dismissInput()
}
func scrollToTop(animated: Bool) {
self.scrollNode.view.setContentOffset(CGPoint(), animated: animated)
}
private func beginReordering(itemNode: MediaPickerSelectedItemNode) {
self.isReordering = true
if let reorderNode = self.reorderNode {
reorderNode.removeFromSupernode()
}
let reorderNode = ReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin)
self.reorderNode = reorderNode
self.addSubnode(reorderNode)
itemNode.isHidden = true
if self.reorderFeedback == nil {
self.reorderFeedback = HapticFeedback()
}
self.reorderFeedback?.impact()
}
private func endReordering(point: CGPoint?) {
if let reorderNode = self.reorderNode {
self.reorderNode = nil
if let itemNode = reorderNode.itemNode, let point = point {
var targetNode: MediaPickerSelectedItemNode?
for (_, node) in self.itemNodes {
if node.frame.contains(point) {
targetNode = node
break
}
}
if let targetNode = targetNode, let targetIndex = self.interaction?.selectionState?.index(of: targetNode.asset) {
self.interaction?.selectionState?.move(itemNode.asset, to: targetIndex)
}
reorderNode.animateCompletion(completion: { [weak reorderNode] in
reorderNode?.removeFromSupernode()
})
self.reorderFeedback?.tap()
} else {
reorderNode.removeFromSupernode()
reorderNode.itemNode?.isHidden = false
}
}
self.isReordering = false
}
private func updateReordering(offset: CGPoint) {
if let reorderNode = self.reorderNode {
reorderNode.updateOffset(offset: offset)
}
}
private var messageNodes: [ListViewItemNode]?
private func updateItems(transition: ContainedViewLayoutTransition) {
guard let (size, insets, items, grouped, theme, wallpaper, bubbleCorners) = self.validLayout else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
var peers = SimpleDictionary<PeerId, Peer>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_MessagePreview, entities: []))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, isCentered: true)
let dragMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_DragToReorder, entities: []))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, isCentered: true)
let headerItems: [ListViewItem] = [previewItem, dragItem]
let params = ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: size.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< headerItems.count {
let itemNode = messageNodes[i]
headerItems[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: size.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< headerItems.count {
var itemNode: ListViewItemNode?
headerItems[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.scrollNode.addSubnode(itemNode!)
}
self.messageNodes = messageNodes
}
var itemSizes: [CGSize] = []
let sideInset: CGFloat = 34.0
let boundingWidth = min(320.0, size.width - insets.left - insets.right - sideInset * 2.0)
var validIds: [String] = []
for item in items {
guard let asset = item as? TGMediaAsset, let identifier = asset.uniqueIdentifier else {
continue
}
validIds.append(identifier)
let itemNode: MediaPickerSelectedItemNode
if let current = self.itemNodes[identifier] {
itemNode = current
} else {
itemNode = MediaPickerSelectedItemNode(asset: asset, interaction: self.interaction)
self.itemNodes[identifier] = itemNode
self.scrollNode.addSubnode(itemNode)
itemNode.setup(size: CGSize(width: boundingWidth, height: boundingWidth))
}
itemNode.update(theme: theme)
itemNode.updateSelectionState()
if !self.isReordering {
itemNode.updateHiddenMedia()
}
if let adjustments = self.interaction?.editingState.adjustments(for: asset), adjustments.cropApplied(forAvatar: false) {
itemSizes.append(adjustments.cropRect.size)
} else {
itemSizes.append(asset.dimensions)
}
}
let boundingSize = CGSize(width: boundingWidth, height: boundingWidth)
var groupLayouts: [([(TGMediaSelectableItem, CGRect, MosaicItemPosition)], CGSize)] = []
if grouped && items.count > 1 {
let groupSize = 10
for i in stride(from: 0, to: itemSizes.count, by: groupSize) {
let sizes = itemSizes[i ..< min(i + groupSize, itemSizes.count)]
let items = items[i ..< min(i + groupSize, items.count)]
if items.count > 1 {
let (mosaicLayout, size) = chatMessageBubbleMosaicLayout(maxSize: boundingSize, itemSizes: Array(sizes), spacing: 1.0, fillWidth: true)
let layout = zip(items, mosaicLayout).map { ($0, $1.0, $1.1) }
groupLayouts.append((layout, size))
} else if let item = items.first, var itemSize = sizes.first {
if itemSize.width > itemSize.height {
itemSize = itemSize.aspectFitted(boundingSize)
} else {
itemSize = boundingSize
}
let itemRect = CGRect(origin: CGPoint(), size: itemSize)
let position: MosaicItemPosition = [.top, .bottom, .left, .right]
groupLayouts.append(([(item, itemRect, position)], itemRect.size))
}
}
} else {
for i in 0 ..< itemSizes.count {
let item = items[i]
var itemSize = itemSizes[i]
if itemSize.width > itemSize.height {
itemSize = itemSize.aspectFitted(boundingSize)
} else {
itemSize = boundingSize
}
let itemRect = CGRect(origin: CGPoint(), size: itemSize)
let position: MosaicItemPosition = [.top, .bottom, .left, .right]
groupLayouts.append(([(item, itemRect, position)], itemRect.size))
}
}
let spacing: CGFloat = 8.0
var contentHeight: CGFloat = 60.0
if let previewNode = self.messageNodes?.first {
transition.updateFrame(node: previewNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + 28.0), size: previewNode.frame.size))
var previewNodeFrame = previewNode.frame
previewNodeFrame.origin.y = size.height - previewNodeFrame.origin.y - previewNodeFrame.size.height
previewNode.updateFrame(previewNodeFrame, within: size, updateFrame: false)
}
var groupIndex = 0
for (items, groupSize) in groupLayouts {
let groupRect = CGRect(origin: CGPoint(x: insets.left + floorToScreenPixels((size.width - insets.left - insets.right - groupSize.width) / 2.0), y: insets.top + contentHeight), size: groupSize)
let groupBackgroundNode: ASImageNode
if let current = self.backgroundNodes[groupIndex] {
groupBackgroundNode = current
} else {
groupBackgroundNode = ASImageNode()
groupBackgroundNode.displaysAsynchronously = false
self.backgroundNodes[groupIndex] = groupBackgroundNode
self.scrollNode.insertSubnode(groupBackgroundNode, at: 0)
}
groupBackgroundNode.image = self.graphics?.chatMessageBackgroundOutgoingExtractedImage
transition.updateFrame(node: groupBackgroundNode, frame: groupRect.insetBy(dx: -6.0, dy: -3.0).offsetBy(dx: 3.0, dy: 0.0))
for (item, itemRect, itemPosition) in items {
if let identifier = item.uniqueIdentifier, let itemNode = self.itemNodes[identifier] {
var corners: CACornerMask = []
if itemPosition.contains(.top) && itemPosition.contains(.left) {
corners.insert(.layerMinXMinYCorner)
}
if itemPosition.contains(.top) && itemPosition.contains(.right) {
corners.insert(.layerMaxXMinYCorner)
}
if itemPosition.contains(.bottom) && itemPosition.contains(.left) {
corners.insert(.layerMinXMaxYCorner)
}
if itemPosition.contains(.bottom) && itemPosition.contains(.right) {
corners.insert(.layerMaxXMaxYCorner)
}
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))
}
}
contentHeight += groupSize.height + spacing
groupIndex += 1
}
if let dragNode = self.messageNodes?.last {
transition.updateAlpha(node: dragNode, alpha: items.count > 1 ? 1.0 : 0.0)
transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 1.0), size: dragNode.frame.size))
var dragNodeFrame = dragNode.frame
dragNodeFrame.origin.y = size.height - dragNodeFrame.origin.y - dragNodeFrame.size.height
dragNode.updateFrame(dragNodeFrame, within: size, updateFrame: false)
contentHeight += 60.0
}
contentHeight += insets.top
contentHeight += insets.bottom
var removeIds: [String] = []
for id in self.itemNodes.keys {
if !validIds.contains(id) {
removeIds.append(id)
}
}
for id in removeIds {
if let itemNode = self.itemNodes.removeValue(forKey: id) {
if transition.isAnimated {
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
} else {
itemNode.removeFromSupernode()
}
}
}
for id in self.backgroundNodes.keys {
if id > groupLayouts.count - 1 {
if let itemNode = self.backgroundNodes.removeValue(forKey: id) {
if transition.isAnimated {
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
} else {
itemNode.removeFromSupernode()
}
}
}
}
if case let .animated(duration, curve) = transition, self.scrollNode.view.contentSize.height > contentHeight {
let maxContentOffset = max(0.0, contentHeight - self.scrollNode.frame.height)
if self.scrollNode.view.contentOffset.y > maxContentOffset {
let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: maxContentOffset), size: self.scrollNode.bounds.size)
let previousBounds = self.scrollNode.bounds
self.scrollNode.bounds = updatedBounds
self.scrollNode.layer.animateBounds(from: previousBounds, to: updatedBounds, duration: duration, timingFunction: curve.timingFunction)
}
}
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
}
func updateSelectionState() {
for (_, itemNode) in self.itemNodes {
itemNode.updateSelectionState()
}
}
func updateHiddenMedia() {
for (_, itemNode) in self.itemNodes {
itemNode.updateHiddenMedia()
}
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, items: [TGMediaSelectableItem], grouped: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, transition: ContainedViewLayoutTransition) {
let previous = self.validLayout
self.validLayout = (size, insets, items, grouped, theme, wallpaper, bubbleCorners)
if previous?.theme !== theme || previous?.wallpaper != wallpaper || previous?.bubbleCorners != bubbleCorners {
self.graphics = PresentationResourcesChat.principalGraphics(theme: theme, wallpaper: wallpaper, bubbleCorners: bubbleCorners)
}
var itemsTransition = transition
if previous?.grouped != grouped {
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
itemsTransition = .immediate
}
let inset: CGFloat = insets.left == 70 ? insets.left : 0.0
self.wallpaperBackgroundNode.update(wallpaper: wallpaper)
self.wallpaperBackgroundNode.updateBubbleTheme(bubbleTheme: theme, bubbleCorners: bubbleCorners)
transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(x: inset, y: 0.0), size: CGSize(width: size.width - inset * 2.0, height: size.height)))
self.wallpaperBackgroundNode.updateLayout(size: CGSize(width: size.width - inset * 2.0, height: size.height), transition: transition)
self.updateItems(transition: itemsTransition)
let bounds = CGRect(origin: CGPoint(), size: size)
transition.updateFrame(node: self.scrollNode, frame: bounds)
}
func transitionView(for identifier: String) -> UIView? {
for (_, itemNode) in self.itemNodes {
if itemNode.asset.uniqueIdentifier == identifier {
return itemNode.transitionView()
}
}
return nil
}
}
private class ReorderingGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: MediaPickerSelectedItemNode?)
private let willBegin: (CGPoint) -> Void
private let began: (MediaPickerSelectedItemNode) -> Void
private let ended: (CGPoint?) -> Void
private let moved: (CGPoint) -> Void
private var initialLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var longPressTimer: SwiftSignalKit.Timer?
private var itemNode: MediaPickerSelectedItemNode?
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: MediaPickerSelectedItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (MediaPickerSelectedItemNode) -> Void, ended: @escaping (CGPoint?) -> Void, moved: @escaping (CGPoint) -> Void) {
self.shouldBegin = shouldBegin
self.willBegin = willBegin
self.began = began
self.ended = ended
self.moved = moved
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.longPressTimer?.invalidate()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func stopLongTapTimer() {
self.itemNode = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func startLongPressTimer() {
self.longPressTimer?.invalidate()
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
self?.longPressTimerFired()
}, queue: Queue.mainQueue())
self.longPressTimer = longPressTimer
longPressTimer.start()
}
private func stopLongPressTimer() {
self.itemNode = nil
self.longPressTimer?.invalidate()
self.longPressTimer = nil
}
override public func reset() {
super.reset()
self.itemNode = nil
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
}
private func longTapTimerFired() {
guard let location = self.initialLocation else {
return
}
self.longTapTimer?.invalidate()
self.longTapTimer = nil
self.willBegin(location)
}
private func longPressTimerFired() {
guard let _ = self.initialLocation else {
return
}
self.state = .began
self.longPressTimer?.invalidate()
self.longPressTimer = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
if let itemNode = self.itemNode {
self.began(itemNode)
}
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.numberOfTouches > 1 {
self.state = .failed
self.ended(nil)
return
}
if self.state == .possible {
if let location = touches.first?.location(in: self.view) {
let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location)
if allowed {
self.itemNode = itemNode
self.initialLocation = location
if requiresLongPress {
self.startLongTapTimer()
self.startLongPressTimer()
} else {
self.state = .began
if let itemNode = self.itemNode {
self.began(itemNode)
}
}
} else {
self.state = .failed
}
} else {
self.state = .failed
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
if let location = touches.first?.location(in: self.view) {
self.ended(location)
} else {
self.ended(nil)
}
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.ended(nil)
self.state = .failed
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
self.state = .changed
self.moved(CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y))
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
let touchLocation = touch.location(in: self.view)
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.state = .failed
}
}
}
}
private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? {
return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
// context.saveGState()
context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor)
var rectCorners: UIRectCorner = []
if corners.contains(.layerMinXMinYCorner) {
rectCorners.insert(.topLeft)
}
if corners.contains(.layerMaxXMinYCorner) {
rectCorners.insert(.topRight)
}
if corners.contains(.layerMinXMaxYCorner) {
rectCorners.insert(.bottomLeft)
}
if corners.contains(.layerMaxXMaxYCorner) {
rectCorners.insert(.bottomRight)
}
let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath
context.addPath(path)
context.fillPath()
// context.restoreGState()
// context.setBlendMode(.clear)
// context.addPath(path)
// context.fillPath()
})?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
}
private final class CopyView: UIView {
let shadow: UIImageView
var snapshotView: UIView?
init(frame: CGRect, corners: CACornerMask, radius: CGFloat) {
self.shadow = UIImageView()
self.shadow.contentMode = .scaleToFill
super.init(frame: frame)
self.shadow.image = generateShadowImage(corners: corners, radius: radius)
self.addSubview(self.shadow)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private final class ReorderingItemNode: ASDisplayNode {
weak var itemNode: MediaPickerSelectedItemNode?
var currentState: (Int, Int)?
private let copyView: CopyView
private let initialLocation: CGPoint
init(itemNode: MediaPickerSelectedItemNode, initialLocation: CGPoint) {
self.itemNode = itemNode
self.copyView = CopyView(frame: CGRect(), corners: itemNode.corners, radius: itemNode.radius)
let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false)
self.initialLocation = initialLocation
super.init()
if let snapshotView = snapshotView {
snapshotView.frame = CGRect(origin: CGPoint(), size: itemNode.bounds.size)
snapshotView.bounds.origin = itemNode.bounds.origin
self.copyView.addSubview(snapshotView)
self.copyView.snapshotView = snapshotView
}
self.view.addSubview(self.copyView)
self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size)
self.copyView.shadow.frame = CGRect(origin: CGPoint(x: -30.0, y: -30.0), size: CGSize(width: itemNode.bounds.size.width + 60.0, height: itemNode.bounds.size.height + 60.0))
self.copyView.shadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.copyView.snapshotView?.layer.animateScale(from: 1.0, to: 1.05, duration: 0.25, removeOnCompletion: false)
self.copyView.shadow.layer.animateScale(from: 1.0, to: 1.05, duration: 0.25, removeOnCompletion: false)
}
func updateOffset(offset: CGPoint) {
self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x + offset.x, y: initialLocation.y + offset.y), size: copyView.bounds.size)
}
func currentOffset() -> CGFloat? {
return self.copyView.center.y
}
func animateCompletion(completion: @escaping () -> Void) {
if let itemNode = self.itemNode {
itemNode.view.superview?.bringSubviewToFront(itemNode.view)
itemNode.layer.animateScale(from: 1.05, to: 1.0, duration: 0.25, removeOnCompletion: false)
let sourceFrame = self.view.convert(self.copyView.frame, to: itemNode.supernode?.view)
let targetFrame = itemNode.frame
itemNode.updateLayout(size: sourceFrame.size, transition: .immediate)
itemNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
completion()
})
itemNode.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.3, curve: .spring))
itemNode.isHidden = false
self.copyView.isHidden = true
} else {
completion()
}
}
}

View file

@ -0,0 +1,94 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import SegmentedControlNode
final class MediaPickerTitleView: UIView {
private let titleNode: ImmediateTextNode
private let segmentedControlNode: SegmentedControlNode
public var theme: PresentationTheme {
didSet {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
}
}
public var title: String = "" {
didSet {
if self.title != oldValue {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)
self.setNeedsLayout()
}
}
}
public var segmentsHidden = true {
didSet {
if self.segmentsHidden != oldValue {
let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut)
transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
}
}
}
public var segments: [String] {
didSet {
if self.segments != oldValue {
self.segmentedControlNode.items = self.segments.map { SegmentedControlItem(title: $0) }
self.setNeedsLayout()
}
}
}
public var index: Int {
get {
return self.segmentedControlNode.selectedIndex
}
set {
self.segmentedControlNode.selectedIndex = newValue
}
}
public var indexUpdated: ((Int) -> Void)?
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
self.theme = theme
self.segments = segments
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex)
self.segmentedControlNode.alpha = 0.0
self.segmentedControlNode.isUserInteractionEnabled = false
super.init(frame: CGRect())
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
self?.indexUpdated?(index)
}
self.addSubnode(self.titleNode)
self.addSubnode(self.segmentedControlNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: min(300.0, size.width - 36.0)), transition: .immediate)
self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize)
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: 44.0))
self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
}
}

View file

@ -9,6 +9,9 @@ swift_library(
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],

View file

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
public struct MosaicItemPosition: OptionSet {
public var rawValue: Int32
@ -34,9 +35,7 @@ private struct MosaicLayoutAttempt {
let heights: [CGFloat]
}
public func chatMessageBubbleMosaicLayout(maxSize: CGSize, itemSizes: [CGSize]) -> ([(CGRect, MosaicItemPosition)], CGSize) {
let spacing: CGFloat = 2.0
public func chatMessageBubbleMosaicLayout(maxSize: CGSize, itemSizes: [CGSize], spacing: CGFloat = 1.0, fillWidth: Bool = false) -> ([(CGRect, MosaicItemPosition)], CGSize) {
var proportions = ""
var averageAspectRatio: CGFloat = 1.0
var forceCalc = false
@ -103,9 +102,14 @@ public func chatMessageBubbleMosaicLayout(maxSize: CGSize, itemSizes: [CGSize])
let thirdHeight = min((maxSize.height - spacing) * 0.5, round(itemInfos[1].aspectRatio * (maxSize.width - spacing) / (itemInfos[2].aspectRatio + itemInfos[1].aspectRatio)))
let secondHeight = maxSize.height - thirdHeight - spacing
let rightWidth = max(minWidth, min((maxSize.width - spacing) * 0.5, round(min(thirdHeight * itemInfos[2].aspectRatio, secondHeight * itemInfos[1].aspectRatio))))
let leftWidth = round(min(firstHeight * itemInfos[0].aspectRatio, (maxSize.width - spacing - rightWidth)))
var rightWidth = max(minWidth, min((maxSize.width - spacing) * 0.5, round(min(thirdHeight * itemInfos[2].aspectRatio, secondHeight * itemInfos[1].aspectRatio))))
if fillWidth {
rightWidth = floorToScreenPixels(maxSize.width / 2.0)
}
var leftWidth = round(min(firstHeight * itemInfos[0].aspectRatio, (maxSize.width - spacing - rightWidth)))
if fillWidth {
leftWidth = maxSize.width - spacing - rightWidth
}
itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: leftWidth, height: firstHeight)
itemInfos[0].position = [.top, .left, .bottom]

View file

@ -1322,7 +1322,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie
(parentController.navigationController as? NavigationController)?.pushViewController(contactsController)
let _ = (contactsController.result
|> deliverOnMainQueue).start(next: { result in
if let (peers, _) = result, let peer = peers.first {
if let (peers, _, _, _) = result, let peer = peers.first {
let dataSignal: Signal<(Peer?, DeviceContactStableId?), NoError>
switch peer {
case let .peer(contact, _, _):

View file

@ -29,6 +29,8 @@ public enum PeerReportOption {
case copyright
case pornography
case childAbuse
case illegalDrugs
case personalDetails
case other
}
@ -59,6 +61,12 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
case .copyright:
title = presentationData.strings.ReportPeer_ReasonCopyright
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportCopyright")
case .illegalDrugs:
title = presentationData.strings.ReportPeer_ReasonIllegalDrugs
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportDrugs")
case .personalDetails:
title = presentationData.strings.ReportPeer_ReasonPersonalDetails
icon = UIImage(bundleImageName: "Chat/Context Menu/User")
case .other:
title = presentationData.strings.ReportPeer_ReasonOther
icon = UIImage(bundleImageName: "Chat/Context Menu/Report")
@ -82,6 +90,10 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
reportReason = .childAbuse
case .copyright:
reportReason = .copyright
case .illegalDrugs:
reportReason = .illegalDrugs
case .personalDetails:
reportReason = .personalDetails
case .other:
reportReason = .custom
}
@ -191,6 +203,10 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
title = presentationData.strings.ReportPeer_ReasonChildAbuse
case .copyright:
title = presentationData.strings.ReportPeer_ReasonCopyright
case .illegalDrugs:
title = presentationData.strings.ReportPeer_ReasonIllegalDrugs
case .personalDetails:
title = presentationData.strings.ReportPeer_ReasonPersonalDetails
case .other:
title = presentationData.strings.ReportPeer_ReasonOther
}
@ -209,6 +225,10 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
reportReason = .childAbuse
case .copyright:
reportReason = .copyright
case .illegalDrugs:
reportReason = .illegalDrugs
case .personalDetails:
reportReason = .personalDetails
case .other:
reportReason = .custom
}

View file

@ -1837,6 +1837,11 @@ public func chatWebpageSnippetFile(account: Account, mediaReference: AnyMediaRef
context.withFlippedContext { c in
c.setBlendMode(.copy)
if let emptyColor = arguments.emptyColor {
c.setFillColor(emptyColor.cgColor)
c.fill(arguments.drawingRect)
}
if arguments.boundingSize.width > arguments.imageSize.width || arguments.boundingSize.height > arguments.imageSize.height {
c.fill(arguments.drawingRect)
}
@ -1849,7 +1854,21 @@ public func chatWebpageSnippetFile(account: Account, mediaReference: AnyMediaRef
return context
} else {
return nil
if let emptyColor = arguments.emptyColor {
let context = DrawingContext(size: arguments.drawingSize, clear: true)
context.withFlippedContext { c in
c.setBlendMode(.copy)
c.setFillColor(emptyColor.cgColor)
c.fill(arguments.drawingRect)
}
addCorners(context, arguments: arguments)
return context
} else {
return nil
}
}
}
}

View file

@ -516,6 +516,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1685456582] = { return Api.ReportReason.parse_inputReportReasonCopyright($0) }
dict[-606798099] = { return Api.ReportReason.parse_inputReportReasonGeoIrrelevant($0) }
dict[-170010905] = { return Api.ReportReason.parse_inputReportReasonFake($0) }
dict[177124030] = { return Api.ReportReason.parse_inputReportReasonIllegalDrugs($0) }
dict[-1631091139] = { return Api.ReportReason.parse_inputReportReasonPersonalDetails($0) }
dict[-247351839] = { return Api.InputEncryptedChat.parse_inputEncryptedChat($0) }
dict[-524237339] = { return Api.PageTableRow.parse_pageTableRow($0) }
dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) }

View file

@ -13010,6 +13010,8 @@ public extension Api {
case inputReportReasonCopyright
case inputReportReasonGeoIrrelevant
case inputReportReasonFake
case inputReportReasonIllegalDrugs
case inputReportReasonPersonalDetails
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -13060,6 +13062,18 @@ public extension Api {
buffer.appendInt32(-170010905)
}
break
case .inputReportReasonIllegalDrugs:
if boxed {
buffer.appendInt32(177124030)
}
break
case .inputReportReasonPersonalDetails:
if boxed {
buffer.appendInt32(-1631091139)
}
break
}
}
@ -13082,6 +13096,10 @@ public extension Api {
return ("inputReportReasonGeoIrrelevant", [])
case .inputReportReasonFake:
return ("inputReportReasonFake", [])
case .inputReportReasonIllegalDrugs:
return ("inputReportReasonIllegalDrugs", [])
case .inputReportReasonPersonalDetails:
return ("inputReportReasonPersonalDetails", [])
}
}
@ -13109,6 +13127,12 @@ public extension Api {
public static func parse_inputReportReasonFake(_ reader: BufferReader) -> ReportReason? {
return Api.ReportReason.inputReportReasonFake
}
public static func parse_inputReportReasonIllegalDrugs(_ reader: BufferReader) -> ReportReason? {
return Api.ReportReason.inputReportReasonIllegalDrugs
}
public static func parse_inputReportReasonPersonalDetails(_ reader: BufferReader) -> ReportReason? {
return Api.ReportReason.inputReportReasonPersonalDetails
}
}
public enum InputEncryptedChat: TypeConstructorDescription {

View file

@ -6264,6 +6264,20 @@ public extension Api {
return result
})
}
public static func resolvePhone(phone: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.contacts.ResolvedPeer>) {
let buffer = Buffer()
buffer.appendInt32(-1963375804)
serializeString(phone, buffer: buffer, boxed: false)
return (FunctionDescription(name: "contacts.resolvePhone", parameters: [("phone", phone)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.contacts.ResolvedPeer? in
let reader = BufferReader(buffer)
var result: Api.contacts.ResolvedPeer?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.contacts.ResolvedPeer
}
return result
})
}
}
public struct help {
public static func test() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {

View file

@ -7,10 +7,10 @@ import MtProtoKit
public enum GetMessagesStrategy {
case local
case cloud
case cloud(skipLocal: Bool)
}
func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud) -> Signal <[Message], NoError> {
func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal <[Message], NoError> {
let postboxSignal = postbox.transaction { transaction -> ([Message], Set<MessageId>, SimpleDictionary<PeerId, Peer>) in
var ids = messageIds
@ -22,21 +22,29 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po
var messages:[Message] = []
var missingMessageIds:Set<MessageId> = Set()
var supportPeers:SimpleDictionary<PeerId, Peer> = SimpleDictionary()
var supportPeers: SimpleDictionary<PeerId, Peer> = SimpleDictionary()
for messageId in ids {
if let message = transaction.getMessage(messageId) {
messages.append(message)
} else {
if case let .cloud(skipLocal) = strategy, skipLocal {
missingMessageIds.insert(messageId)
if let peer = transaction.getPeer(messageId.peerId) {
supportPeers[messageId.peerId] = peer
}
} else {
if let message = transaction.getMessage(messageId) {
messages.append(message)
} else {
missingMessageIds.insert(messageId)
if let peer = transaction.getPeer(messageId.peerId) {
supportPeers[messageId.peerId] = peer
}
}
}
}
return (messages, missingMessageIds, supportPeers)
}
if strategy == .cloud {
if case .cloud = strategy {
return postboxSignal
|> mapToSignal { (existMessages, missingMessageIds, supportPeers) in

View file

@ -10,7 +10,7 @@ public enum SearchMessagesLocation: Equatable {
case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?)
case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, topMsgId: MessageId?, minDate: Int32?, maxDate: Int32?)
case publicForwards(messageId: MessageId, datacenterId: Int?)
case recentDocuments
case sentMedia(tags: MessageTags?)
}
private struct SearchMessagesPeerState: Equatable {
@ -339,42 +339,20 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation
return .single((nil, nil))
}
}
case .recentDocuments:
let filter: Api.MessagesFilter = messageFilterForTagMask(.file) ?? .inputMessagesFilterEmpty
let peerId = account.peerId
case let .sentMedia(tags):
let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty
remoteSearchResult = account.postbox.transaction { transaction -> Peer? in
guard let peer = transaction.getPeer(peerId) else {
return nil
let peerMessages: Signal<Api.messages.Messages?, NoError>
if let completed = state?.main.completed, completed {
peerMessages = .single(nil)
} else {
peerMessages = account.network.request(Api.functions.messages.searchSentMedia(q: query, filter: filter, limit: limit))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
return peer
}
|> mapToSignal { peer -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in
guard let peer = peer else {
return .single((nil, nil))
}
let inputPeer = Api.InputPeer.inputPeerEmpty
var flags: Int32 = 0
let fromInputPeer = apiInputPeer(peer)
flags |= (1 << 0)
let peerMessages: Signal<Api.messages.Messages?, NoError>
if let completed = state?.main.completed, completed {
peerMessages = .single(nil)
} else {
let lowerBound = state?.main.messages.last.flatMap({ $0.index })
let signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, topMsgId: nil, filter: filter, minDate: 0, maxDate: (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0))
peerMessages = signal
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
}
return combineLatest(peerMessages, .single(nil))
}
remoteSearchResult = combineLatest(peerMessages, .single(nil))
}
return remoteSearchResult

View file

@ -120,7 +120,7 @@ public extension TelegramEngine {
return _internal_markAllChatsAsRead(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager)
}
public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud) -> Signal <[Message], NoError> {
public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal <[Message], NoError> {
return _internal_getMessagesLoadIfNecessary(messageIds, postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, strategy: strategy)
}

View file

@ -83,6 +83,8 @@ public enum ReportReason: Equatable {
case childAbuse
case copyright
case irrelevantLocation
case illegalDrugs
case personalDetails
case custom
}
@ -103,6 +105,10 @@ private extension ReportReason {
return .inputReportReasonCopyright
case .irrelevantLocation:
return .inputReportReasonGeoIrrelevant
case .illegalDrugs:
return .inputReportReasonIllegalDrugs
case .personalDetails:
return .inputReportReasonPersonalDetails
case .custom:
return .inputReportReasonOther
}

View file

@ -46,9 +46,9 @@ public extension ToolbarTheme {
}
public extension NavigationBarTheme {
convenience init(rootControllerTheme: PresentationTheme, enableBackgroundBlur: Bool = true, hideBackground: Bool = false, hideBadge: Bool = false) {
convenience init(rootControllerTheme: PresentationTheme, enableBackgroundBlur: Bool = true, hideBackground: Bool = false, hideBadge: Bool = false, hideSeparator: Bool = false) {
let theme = rootControllerTheme.rootController.navigationBar
self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor)
self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor)
}
}
@ -63,8 +63,8 @@ public extension NavigationBarPresentationData {
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
}
convenience init(presentationData: PresentationData, hideBackground: Bool, hideBadge: Bool) {
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground, hideBadge: hideBadge), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
convenience init(presentationData: PresentationData, hideBackground: Bool, hideBadge: Bool, hideSeparator: Bool = false) {
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground, hideBadge: hideBadge, hideSeparator: hideSeparator), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
}
convenience init(presentationTheme: PresentationTheme, presentationStrings: PresentationStrings) {

View file

@ -1478,4 +1478,10 @@ public final class PresentationTheme: Equatable {
public func withUpdated(preview: Bool) -> PresentationTheme {
return PresentationTheme(name: self.name, index: self.index, referenceTheme: self.referenceTheme, overallDarkAppearance: self.overallDarkAppearance, intro: self.intro, passcode: self.passcode, rootController: self.rootController, list: self.list, chatList: self.chatList, chat: self.chat, actionSheet: self.actionSheet, contextMenu: self.contextMenu, inAppNotification: self.inAppNotification, chart: self.chart, preview: preview)
}
public func withInvertedBlocksBackground() -> PresentationTheme {
let list = self.list.withUpdated(blocksBackgroundColor: self.list.itemBlocksBackgroundColor, itemBlocksBackgroundColor: self.list.blocksBackgroundColor)
return PresentationTheme(name: self.name, index: self.index, referenceTheme: self.referenceTheme, overallDarkAppearance: self.overallDarkAppearance, intro: self.intro, passcode: self.passcode, rootController: self.rootController, list: list, chatList: self.chatList, chat: self.chat, actionSheet: self.actionSheet, contextMenu: self.contextMenu, inAppNotification: self.inAppNotification, chart: self.chart, preview: self.preview)
}
}

View file

@ -263,6 +263,7 @@ swift_library(
"//submodules/Pasteboard:Pasteboard",
"//submodules/ChatSendMessageActionUI:ChatSendMessageActionUI",
"//submodules/ChatTextLinkEditUI:ChatTextLinkEditUI",
"//submodules/MediaPickerUI:MediaPickerUI",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Icon-3.pdf",
"filename" : "Icon-5.pdf",
"idiom" : "universal"
}
],

View file

@ -1,107 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.000000 5.000000 cm
0.635294 0.635294 0.635294 scn
0.000000 12.652475 m
0.000000 13.439779 0.000000 13.833430 0.049142 14.163219 c
0.340950 16.121538 1.878462 17.659050 3.836780 17.950859 c
4.166570 18.000000 4.560222 18.000000 5.347525 18.000000 c
5.881966 18.000000 l
6.567171 18.000000 7.193567 18.387135 7.500000 19.000000 c
7.806434 19.612865 8.432830 20.000000 9.118035 20.000000 c
14.881966 20.000000 l
15.567171 20.000000 16.193567 19.612865 16.500000 19.000000 c
16.806433 18.387135 17.432829 18.000000 18.118034 18.000000 c
18.652475 18.000000 l
19.439775 18.000000 19.833426 18.000000 20.163214 17.950859 c
22.121534 17.659052 23.659048 16.121538 23.950855 14.163218 c
23.999996 13.833429 23.999996 13.439779 23.999996 12.652477 c
23.999996 7.200001 l
23.999996 4.679764 23.999996 3.419645 23.509525 2.457043 c
23.078094 1.610313 22.389683 0.921902 21.542953 0.490471 c
20.580351 0.000000 19.320232 0.000000 16.799995 0.000000 c
7.199999 0.000000 l
4.679763 0.000000 3.419646 0.000000 2.457043 0.490471 c
1.610313 0.921902 0.921901 1.610313 0.490471 2.457043 c
0.000000 3.419645 0.000000 4.679764 0.000000 7.200000 c
0.000000 12.652475 l
h
12.000000 12.375000 m
10.136039 12.375000 8.625000 10.863961 8.625000 9.000000 c
8.625000 7.136039 10.136039 5.625000 12.000000 5.625000 c
13.863961 5.625000 15.375000 7.136039 15.375000 9.000000 c
15.375000 10.863961 13.863961 12.375000 12.000000 12.375000 c
h
6.375000 9.000000 m
6.375000 12.106602 8.893398 14.625000 12.000000 14.625000 c
15.106602 14.625000 17.625000 12.106602 17.625000 9.000000 c
17.625000 5.893398 15.106602 3.375000 12.000000 3.375000 c
8.893398 3.375000 6.375000 5.893398 6.375000 9.000000 c
h
19.500000 12.000000 m
20.328426 12.000000 21.000000 12.671573 21.000000 13.500000 c
21.000000 14.328427 20.328426 15.000000 19.500000 15.000000 c
18.671574 15.000000 18.000000 14.328427 18.000000 13.500000 c
18.000000 12.671573 18.671574 12.000000 19.500000 12.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
2018
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002108 00000 n
0000002131 00000 n
0000002304 00000 n
0000002378 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2437
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Icon-7.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,180 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.334961 4.334656 cm
0.000000 0.478431 1.000000 scn
7.922659 16.330362 m
7.990485 16.330341 l
11.339519 16.330341 l
11.407344 16.330362 l
11.830773 16.330563 12.150443 16.330715 12.458707 16.256708 c
12.730634 16.191423 12.990590 16.083746 13.229033 15.937628 c
13.499336 15.771986 13.725266 15.545843 14.024528 15.246298 c
14.024536 15.246290 l
14.072486 15.198310 l
14.523190 14.747606 l
14.550632 14.720162 14.558728 14.712102 14.566200 14.704887 c
14.809235 14.470261 15.132179 14.336493 15.469935 14.330548 c
15.480320 14.330365 15.491745 14.330341 15.530556 14.330341 c
15.544931 14.330341 l
15.655980 14.330346 15.729312 14.330349 15.793977 14.328072 c
17.716871 14.260361 19.260023 12.717211 19.327732 10.794315 c
19.330008 10.729712 19.330006 10.656460 19.330002 10.545589 c
19.330002 10.545401 l
19.330002 10.530893 l
19.330002 5.465342 l
19.330002 5.436434 l
19.330002 5.436368 l
19.330008 4.620798 19.330011 3.968183 19.286921 3.440796 c
19.242689 2.899414 19.149759 2.431705 18.930542 2.001467 c
18.579166 1.311852 18.018492 0.751179 17.328878 0.399803 c
16.898640 0.180586 16.430929 0.087656 15.889549 0.043423 c
15.362137 0.000332 14.709483 0.000336 13.893861 0.000341 c
13.865003 0.000341 l
5.465001 0.000341 l
5.436143 0.000341 l
4.620519 0.000336 3.967866 0.000332 3.440454 0.043423 c
2.899074 0.087656 2.431364 0.180586 2.001126 0.399803 c
1.311511 0.751179 0.750838 1.311852 0.399462 2.001467 c
0.180244 2.431705 0.087314 2.899414 0.043082 3.440796 c
-0.000010 3.968214 -0.000005 4.620880 0.000001 5.436520 c
0.000001 5.465342 l
0.000001 10.530894 l
0.000000 10.545269 l
-0.000004 10.656319 -0.000006 10.729650 0.002271 10.794315 c
0.069981 12.717211 1.613132 14.260361 3.536026 14.328072 c
3.600692 14.330349 3.674023 14.330346 3.785072 14.330341 c
3.799448 14.330341 l
3.838259 14.330341 3.849683 14.330365 3.860067 14.330548 c
4.197824 14.336493 4.520769 14.470261 4.763803 14.704887 c
4.771275 14.712102 4.779370 14.720162 4.806813 14.747605 c
5.257516 15.198309 l
5.305459 15.246283 l
5.604728 15.545835 5.830662 15.771983 6.100970 15.937628 c
6.339413 16.083746 6.599370 16.191423 6.871296 16.256708 c
7.179560 16.330715 7.499230 16.330563 7.922659 16.330362 c
h
7.990485 15.000341 m
7.469651 15.000341 7.317619 14.996068 7.181778 14.963455 c
7.045560 14.930752 6.915338 14.876812 6.795893 14.803617 c
6.676778 14.730623 6.566254 14.626143 6.197969 14.257857 c
5.747265 13.807154 l
5.743656 13.803544 l
5.721325 13.781212 5.704509 13.764395 5.687559 13.748032 c
5.202402 13.279657 4.557722 13.012622 3.883473 13.000754 c
3.859918 13.000340 3.836137 13.000340 3.804559 13.000341 c
3.804546 13.000341 l
3.804533 13.000341 l
3.799448 13.000341 l
3.668953 13.000341 3.620954 13.000237 3.582830 12.998896 c
2.357739 12.955757 1.374586 11.972603 1.331447 10.747512 c
1.330105 10.709388 1.330001 10.661388 1.330001 10.530894 c
1.330001 5.465342 l
1.330001 4.614290 1.330518 4.015995 1.368665 3.549099 c
1.406177 3.089968 1.476738 2.816771 1.584501 2.605274 c
1.808365 2.165915 2.165575 1.808706 2.604933 1.584841 c
2.816429 1.477079 3.089627 1.406519 3.548759 1.369005 c
4.015654 1.330858 4.613949 1.330341 5.465001 1.330341 c
13.865003 1.330341 l
14.716054 1.330341 15.314349 1.330858 15.781244 1.369005 c
16.240376 1.406519 16.513573 1.477079 16.725071 1.584841 c
17.164429 1.808706 17.521639 2.165915 17.745502 2.605274 c
17.853266 2.816771 17.923826 3.089968 17.961338 3.549099 c
17.999485 4.015995 18.000002 4.614290 18.000002 5.465342 c
18.000002 10.530893 l
18.000002 10.661388 17.999899 10.709388 17.998556 10.747512 c
17.955418 11.972603 16.972263 12.955757 15.747173 12.998896 c
15.709049 13.000237 15.661050 13.000341 15.530556 13.000341 c
15.525471 13.000341 l
15.525459 13.000341 l
15.525448 13.000341 l
15.493867 13.000340 15.470086 13.000340 15.446530 13.000754 c
14.772282 13.012622 14.127602 13.279657 13.642445 13.748032 c
13.625524 13.764366 13.608735 13.781154 13.586460 13.803428 c
13.586380 13.803510 l
13.586320 13.803570 l
13.582737 13.807155 l
13.132033 14.257858 l
12.763749 14.626143 12.653225 14.730623 12.534110 14.803617 c
12.414665 14.876812 12.284443 14.930752 12.148225 14.963455 c
12.012384 14.996068 11.860353 15.000341 11.339519 15.000341 c
7.990485 15.000341 l
h
9.665002 10.500341 m
7.961203 10.500341 6.580001 9.119140 6.580001 7.415341 c
6.580001 5.711544 7.961203 4.330341 9.665002 4.330341 c
11.368801 4.330341 12.750002 5.711544 12.750002 7.415341 c
12.750002 9.119140 11.368801 10.500341 9.665002 10.500341 c
h
5.250001 7.415341 m
5.250001 9.853679 7.226664 11.830341 9.665002 11.830341 c
12.103339 11.830341 14.080002 9.853679 14.080002 7.415341 c
14.080002 4.977005 12.103339 3.000341 9.665002 3.000341 c
7.226664 3.000341 5.250001 4.977005 5.250001 7.415341 c
h
15.665001 9.665341 m
16.217285 9.665341 16.665001 10.113056 16.665001 10.665341 c
16.665001 11.217627 16.217285 11.665341 15.665001 11.665341 c
15.112716 11.665341 14.665001 11.217627 14.665001 10.665341 c
14.665001 10.113056 15.112716 9.665341 15.665001 9.665341 c
h
f*
n
Q
endstream
endobj
3 0 obj
5070
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000005160 00000 n
0000005183 00000 n
0000005356 00000 n
0000005430 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5489
%%EOF

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Icon-4.pdf",
"filename" : "Type=Default-5.pdf",
"idiom" : "universal"
}
],

View file

@ -1,157 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.500000 4.499939 cm
0.635294 0.635294 0.635294 scn
8.325001 21.000061 m
8.276846 21.000061 l
7.057586 21.000072 6.074163 21.000082 5.277799 20.935015 c
4.457843 20.868023 3.737631 20.726484 3.071304 20.386972 c
2.012892 19.847685 1.152376 18.987169 0.613089 17.928757 c
0.273578 17.262430 0.132038 16.542217 0.065045 15.722262 c
-0.000020 14.925898 -0.000011 13.942474 0.000000 12.723215 c
0.000000 12.675060 l
0.000000 8.325061 l
0.000000 8.276906 l
-0.000011 7.057647 -0.000020 6.074224 0.065045 5.277859 c
0.132038 4.457905 0.273578 3.737692 0.613089 3.071363 c
1.152376 2.012953 2.012892 1.152437 3.071304 0.613150 c
3.737631 0.273638 4.457843 0.132099 5.277799 0.065105 c
6.074152 0.000040 7.057559 0.000050 8.276797 0.000061 c
8.276814 0.000061 l
8.325001 0.000061 l
12.675000 0.000061 l
12.723186 0.000061 l
12.723204 0.000061 l
13.942441 0.000050 14.925848 0.000040 15.722202 0.065105 c
16.542156 0.132099 17.262369 0.273638 17.928698 0.613150 c
18.987108 1.152437 19.847624 2.012953 20.386911 3.071363 c
20.726423 3.737692 20.867962 4.457905 20.934956 5.277860 c
21.000021 6.074213 21.000011 7.057621 21.000000 8.276858 c
21.000000 8.276875 l
21.000000 8.325062 l
21.000000 12.675060 l
21.000000 12.723248 l
21.000000 12.723265 l
21.000011 13.942501 21.000021 14.925909 20.934956 15.722262 c
20.867962 16.542217 20.726423 17.262430 20.386911 17.928757 c
19.847624 18.987169 18.987108 19.847685 17.928698 20.386972 c
17.262369 20.726484 16.542156 20.868023 15.722201 20.935015 c
14.925837 21.000082 13.942413 21.000072 12.723154 21.000061 c
12.674999 21.000061 l
8.325001 21.000061 l
h
9.000000 16.800060 m
9.000000 17.220100 9.000000 17.430120 9.081745 17.590553 c
9.153650 17.731674 9.268385 17.846411 9.409508 17.918316 c
9.569941 18.000061 9.779961 18.000061 10.200000 18.000061 c
10.799999 18.000061 l
11.220039 18.000061 11.430059 18.000061 11.590492 17.918316 c
11.731615 17.846411 11.846350 17.731674 11.918255 17.590553 c
12.000000 17.430120 12.000000 17.220100 12.000000 16.800060 c
12.000000 4.200062 l
12.000000 3.780022 12.000000 3.570002 11.918255 3.409569 c
11.846350 3.268448 11.731615 3.153711 11.590492 3.081806 c
11.430059 3.000061 11.220039 3.000061 10.800000 3.000061 c
10.200001 3.000061 l
9.779961 3.000061 9.569941 3.000061 9.409508 3.081806 c
9.268385 3.153711 9.153650 3.268448 9.081745 3.409569 c
9.000000 3.570002 9.000000 3.780022 9.000000 4.200062 c
9.000000 16.800060 l
h
3.750000 12.300061 m
3.750000 12.720100 3.750000 12.930120 3.831745 13.090553 c
3.903650 13.231675 4.018386 13.346411 4.159507 13.418316 c
4.319942 13.500061 4.529961 13.500061 4.950000 13.500061 c
5.550001 13.500061 l
5.970040 13.500061 6.180060 13.500061 6.340494 13.418316 c
6.481615 13.346411 6.596350 13.231675 6.668255 13.090553 c
6.750000 12.930120 6.750000 12.720100 6.750000 12.300060 c
6.750000 4.200062 l
6.750000 3.780022 6.750000 3.570002 6.668255 3.409569 c
6.596350 3.268448 6.481615 3.153711 6.340494 3.081806 c
6.180060 3.000061 5.970040 3.000061 5.550001 3.000061 c
4.950000 3.000061 l
4.529961 3.000061 4.319942 3.000061 4.159507 3.081806 c
4.018386 3.153711 3.903650 3.268448 3.831745 3.409569 c
3.750000 3.570002 3.750000 3.780022 3.750000 4.200062 c
3.750000 12.300061 l
h
14.331745 8.590554 m
14.250000 8.430120 14.250000 8.220100 14.250000 7.800061 c
14.250000 4.200062 l
14.250000 3.780022 14.250000 3.570002 14.331745 3.409569 c
14.403650 3.268448 14.518386 3.153711 14.659507 3.081806 c
14.819941 3.000061 15.029961 3.000061 15.450000 3.000061 c
16.049999 3.000061 l
16.470039 3.000061 16.680059 3.000061 16.840492 3.081806 c
16.981613 3.153711 17.096350 3.268448 17.168255 3.409569 c
17.250000 3.570002 17.250000 3.780022 17.250000 4.200062 c
17.250000 7.800061 l
17.250000 8.220100 17.250000 8.430120 17.168255 8.590554 c
17.096350 8.731675 16.981613 8.846411 16.840492 8.918316 c
16.680059 9.000061 16.470039 9.000061 16.049999 9.000061 c
15.450000 9.000061 l
15.029961 9.000061 14.819941 9.000061 14.659507 8.918316 c
14.518386 8.846411 14.403650 8.731675 14.331745 8.590554 c
h
f*
n
Q
endstream
endobj
3 0 obj
4096
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004186 00000 n
0000004209 00000 n
0000004382 00000 n
0000004456 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4515
%%EOF

View file

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.000000 3.000000 cm
0.000000 0.000000 0.000000 scn
0.653961 20.723944 m
0.000000 19.440472 0.000000 17.760315 0.000000 14.400000 c
0.000000 9.600000 l
0.000000 6.239685 0.000000 4.559526 0.653961 3.276056 c
1.229201 2.147085 2.147084 1.229200 3.276057 0.653961 c
4.559527 0.000000 6.239685 0.000000 9.600000 0.000000 c
14.400000 0.000000 l
17.760315 0.000000 19.440474 0.000000 20.723944 0.653961 c
21.852915 1.229200 22.770800 2.147085 23.346039 3.276056 c
24.000000 4.559526 24.000000 6.239685 24.000000 9.600000 c
24.000000 14.400000 l
24.000000 17.760315 24.000000 19.440472 23.346039 20.723944 c
22.770800 21.852915 21.852915 22.770800 20.723944 23.346039 c
19.440474 24.000000 17.760315 24.000000 14.400000 24.000000 c
9.600000 24.000000 l
6.239685 24.000000 4.559527 24.000000 3.276057 23.346039 c
2.147084 22.770800 1.229201 21.852915 0.653961 20.723944 c
h
11.500000 20.000000 m
10.947716 20.000000 10.500000 19.552284 10.500000 19.000000 c
10.500000 5.000000 l
10.500000 4.447716 10.947715 4.000000 11.500000 4.000000 c
12.500000 4.000000 l
13.052284 4.000000 13.500000 4.447716 13.500000 5.000000 c
13.500000 19.000000 l
13.500000 19.552284 13.052284 20.000000 12.500000 20.000000 c
11.500000 20.000000 l
h
5.000000 14.000000 m
4.447715 14.000000 4.000000 13.552284 4.000000 13.000000 c
4.000000 5.000000 l
4.000000 4.447716 4.447715 4.000000 5.000000 4.000000 c
6.000000 4.000000 l
6.552284 4.000000 7.000000 4.447716 7.000000 5.000000 c
7.000000 13.000000 l
7.000000 13.552285 6.552285 14.000000 6.000000 14.000000 c
5.000000 14.000000 l
h
17.000000 9.000000 m
17.000000 9.552284 17.447716 10.000000 18.000000 10.000000 c
19.000000 10.000000 l
19.552284 10.000000 20.000000 9.552284 20.000000 9.000000 c
20.000000 5.000000 l
20.000000 4.447716 19.552284 4.000000 19.000000 4.000000 c
18.000000 4.000000 l
17.447716 4.000000 17.000000 4.447716 17.000000 5.000000 c
17.000000 9.000000 l
h
f*
n
Q
endstream
endobj
3 0 obj
1975
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002065 00000 n
0000002088 00000 n
0000002261 00000 n
0000002335 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2394
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Icon-6.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,152 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.334961 3.839600 cm
0.000000 0.000000 0.000000 scn
5.500002 14.986980 m
5.376047 14.998394 5.192461 15.000357 4.839518 15.000357 c
3.865001 15.000357 l
3.293976 15.000357 2.905700 14.999840 2.605557 14.975317 c
2.313177 14.951428 2.163465 14.908116 2.058924 14.854851 c
1.807728 14.726860 1.603498 14.522631 1.475508 14.271434 c
1.422241 14.166893 1.378930 14.017180 1.355041 13.724800 c
1.330518 13.424658 1.330001 13.036383 1.330001 12.465357 c
1.330001 3.865356 l
1.330001 3.294331 1.330518 2.906055 1.355041 2.605912 c
1.378929 2.313533 1.422241 2.163820 1.475508 2.059279 c
1.603498 1.808083 1.807728 1.603854 2.058924 1.475863 c
2.163465 1.422597 2.313177 1.379285 2.605557 1.355396 c
2.905700 1.330873 3.293976 1.330357 3.865001 1.330357 c
9.465001 1.330357 l
10.036027 1.330357 10.424302 1.330873 10.724444 1.355396 c
11.016825 1.379285 11.166537 1.422597 11.271078 1.475863 c
11.522275 1.603854 11.726503 1.808083 11.854495 2.059279 c
11.907761 2.163820 11.951073 2.313533 11.974961 2.605913 c
11.999484 2.906056 12.000001 3.294332 12.000001 3.865356 c
12.000001 7.839873 l
12.000001 8.192817 11.998038 8.376402 11.986625 8.500357 c
9.365002 8.500357 l
9.337519 8.500357 l
8.800825 8.500348 8.357979 8.500341 7.997253 8.529814 c
7.622625 8.560422 7.278400 8.626103 6.955117 8.790825 c
6.453665 9.046327 6.045972 9.454020 5.790470 9.955472 c
5.625749 10.278755 5.560067 10.622980 5.529459 10.997608 c
5.499986 11.358337 5.499993 11.801186 5.500001 12.337883 c
5.500002 12.365356 l
5.500002 14.986980 l
h
11.059549 9.830357 m
6.830002 14.059904 l
6.830002 12.365356 l
6.830002 11.794331 6.830519 11.406055 6.855042 11.105913 c
6.878930 10.813533 6.922242 10.663820 6.975508 10.559279 c
7.103499 10.308083 7.307728 10.103853 7.558925 9.975863 c
7.663465 9.922597 7.813178 9.879285 8.105557 9.855396 c
8.405701 9.830873 8.793976 9.830357 9.365002 9.830357 c
11.059549 9.830357 l
h
5.958706 16.256723 m
5.650443 16.330730 5.330773 16.330578 4.907343 16.330378 c
4.839518 16.330357 l
3.865001 16.330357 l
3.837527 16.330357 l
3.300830 16.330366 2.857981 16.330372 2.497253 16.300900 c
2.122625 16.270292 1.778399 16.204611 1.455117 16.039888 c
0.953665 15.784387 0.545971 15.376694 0.290469 14.875241 c
0.125748 14.551959 0.060067 14.207733 0.029458 13.833105 c
-0.000014 13.472384 -0.000007 13.029548 0.000000 12.492866 c
0.000000 12.492830 l
0.000000 12.465357 l
0.000000 3.865356 l
0.000000 3.837883 l
0.000000 3.837849 l
-0.000007 3.301166 -0.000014 2.858329 0.029458 2.497608 c
0.060067 2.122980 0.125748 1.778755 0.290469 1.455472 c
0.545971 0.954020 0.953665 0.546327 1.455117 0.290825 c
1.778399 0.126104 2.122625 0.060423 2.497253 0.029814 c
2.857968 0.000343 3.300798 0.000349 3.837470 0.000357 c
3.837543 0.000357 l
3.865001 0.000357 l
9.465001 0.000357 l
9.492460 0.000357 l
9.492532 0.000357 l
10.029204 0.000349 10.472034 0.000343 10.832749 0.029814 c
11.207377 0.060423 11.551602 0.126104 11.874886 0.290825 c
12.376338 0.546327 12.784031 0.954020 13.039534 1.455472 c
13.204254 1.778755 13.269936 2.122980 13.300544 2.497608 c
13.330016 2.858333 13.330009 3.301176 13.330001 3.837869 c
13.330001 3.837897 l
13.330001 3.865356 l
13.330001 7.839873 l
13.330022 7.907694 l
13.330022 7.907717 l
13.330222 8.331139 13.330374 8.650803 13.256367 8.959062 c
13.191083 9.230988 13.083405 9.490944 12.937287 9.729388 c
12.771642 9.999696 12.545493 10.225630 12.245939 10.524900 c
12.197968 10.572842 l
7.572486 15.198324 l
7.524543 15.246297 l
7.524540 15.246299 l
7.225273 15.545851 6.999340 15.771998 6.729033 15.937643 c
6.490589 16.083761 6.230633 16.191439 5.958706 16.256723 c
h
f*
n
Q
endstream
endobj
3 0 obj
3669
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003759 00000 n
0000003782 00000 n
0000003955 00000 n
0000004029 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4088
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "drugs_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,97 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
0.707107 -0.707107 0.707107 0.707107 -0.794397 7.217199 cm
0.000000 0.000000 0.000000 scn
5.665000 23.093994 m
2.536307 23.093994 0.000000 20.557688 0.000000 17.428993 c
0.000000 11.626219 l
0.000000 10.428994 l
0.000000 7.428994 l
0.000000 4.300301 2.536307 1.763992 5.665000 1.763992 c
8.793693 1.763992 11.330000 4.300301 11.330000 7.428994 c
11.330000 10.428994 l
11.330000 11.626219 l
11.330000 17.428993 l
11.330000 20.557688 8.793693 23.093994 5.665000 23.093994 c
h
10.000000 13.456901 m
10.000000 17.428993 l
10.000000 19.823149 8.059155 21.763994 5.665000 21.763994 c
3.270846 21.763994 1.330000 19.823149 1.330000 17.428993 c
1.330000 13.456901 l
2.192297 13.760678 3.608150 14.093994 5.665000 14.093994 c
7.721851 14.093994 9.137704 13.760677 10.000000 13.456901 c
h
1.330000 11.626219 m
1.330000 10.428994 l
1.330000 7.428994 l
1.330000 5.034840 3.270846 3.093994 5.665000 3.093994 c
8.059155 3.093994 10.000000 5.034840 10.000000 7.428994 c
10.000000 10.428994 l
10.000000 11.626219 l
10.000000 11.915585 9.860453 12.089630 9.704794 12.148787 c
9.019821 12.409109 7.712020 12.763994 5.665000 12.763994 c
3.617980 12.763994 2.310179 12.409109 1.625207 12.148788 c
1.469548 12.089630 1.330000 11.915585 1.330000 11.626219 c
h
f*
n
Q
endstream
endobj
3 0 obj
1275
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001365 00000 n
0000001388 00000 n
0000001561 00000 n
0000001635 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1694
%%EOF

View file

@ -13,14 +13,19 @@ import AccountContext
import ItemListPeerActionItem
import AttachmentUI
import TelegramStringFormatting
import ListMessageItem
private final class AttachmentFileControllerArguments {
let context: AccountContext
let openGallery: () -> Void
let openFiles: () -> Void
let send: (Message) -> Void
init(openGallery: @escaping () -> Void, openFiles: @escaping () -> Void) {
init(context: AccountContext, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, send: @escaping (Message) -> Void) {
self.context = context
self.openGallery = openGallery
self.openFiles = openFiles
self.send = send
}
}
@ -29,7 +34,10 @@ private enum AttachmentFileSection: Int32 {
case recent
}
private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool {
private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool {
guard let lhsMessage = lhsMessage, let rhsMessage = rhsMessage else {
return lhsMessage == nil && rhsMessage == nil
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
@ -44,7 +52,7 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
case selectFromFiles(PresentationTheme, String)
case recentHeader(PresentationTheme, String)
case file(Int32, PresentationTheme, Message)
case file(Int32, PresentationTheme, Message?)
var section: ItemListSectionId {
switch self {
@ -89,7 +97,7 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
return false
}
case let .file(lhsIndex, lhsTheme, lhsMessage):
if case let .file(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex != rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
if case let .file(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
return true
} else {
return false
@ -115,33 +123,38 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
case let .recentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .file(_, _, message):
let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile
let label: String
if let file = file {
label = dataSizeString(file.size ?? 0, formatting: DataSizeStringFormatting(strings: presentationData.strings, decimalSeparator: "."))
} else {
label = ""
}
return ItemListDisclosureItem(presentationData: presentationData, title: file?.fileName ?? "", label: label, sectionId: self.section, style: .blocks, action: {
})
let interaction = ListMessageItemInteraction(openMessage: { message, _ in
arguments.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, context: arguments.context, chatLocation: .peer(PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, displayFileInfo: false, displayBackground: true, style: .blocks)
}
}
}
private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?) -> [AttachmentFileEntry] {
var entries: [AttachmentFileEntry] = []
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
if let _ = recentDocuments {
// entries.append(.recentHeader(presentationData.theme, "RECENTLY SENT FILES".uppercased()))
// var i: Int32 = 0
// for file in recentDocuments {
// entries.append(.file(i, presentationData.theme, file))
// i += 1
// }
if let recentDocuments = recentDocuments {
if recentDocuments.count > 0 {
entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
var i: Int32 = 0
for file in recentDocuments {
entries.append(.file(i, presentationData.theme, file))
i += 1
}
}
} else {
entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
for i in 0 ..< 8 {
entries.append(.file(Int32(i), presentationData.theme, nil))
}
}
return entries
@ -149,32 +162,116 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
private class AttachmentFileControllerImpl: ItemListController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
var prepareForReuseImpl: () -> Void = {}
public func prepareForReuse() {
self.prepareForReuseImpl()
self.scrollToTop?()
}
}
public func attachmentFileController(context: AccountContext, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void) -> AttachmentContainable {
private struct AttachmentFileControllerState: Equatable {
var searching: Bool
}
public func attachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentContainable {
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true)
let stateValue = Atomic(value: AttachmentFileControllerState(searching: false))
let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var expandImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
let arguments = AttachmentFileControllerArguments(openGallery: {
presentGallery()
}, openFiles: {
presentFiles()
})
var dismissInputImpl: (() -> Void)?
let arguments = AttachmentFileControllerArguments(
context: context,
openGallery: {
presentGallery()
},
openFiles: {
presentFiles()
},
send: { message in
let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
|> deliverOnMainQueue).start(next: { messages in
if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.message(message: MessageReference(message), media: file))
}
dismissImpl?()
})
}
)
let recentDocuments: Signal<[Message]?, NoError> = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .recentDocuments, query: "", state: nil)
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, recentDocuments)
|> map { presentationData, recentDocuments -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previousRecentDocuments = Atomic<[Message]?>(value: nil)
let signal = combineLatest(queue: Queue.mainQueue(),
presentationData,
recentDocuments,
statePromise.get()
)
|> map { presentationData, recentDocuments, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
if presentationData.theme.list.blocksBackgroundColor.rgb == 0x000000 {
let updatedTheme = presentationData.theme.withInvertedBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
}
let previousRecentDocuments = previousRecentDocuments.swap(recentDocuments)
let crossfade = previousRecentDocuments == nil && recentDocuments != nil
var animateChanges = false
if let previousRecentDocuments = previousRecentDocuments, let recentDocuments = recentDocuments, !previousRecentDocuments.isEmpty && !recentDocuments.isEmpty, !crossfade {
animateChanges = true
}
var rightNavigationButton: ItemListNavigationButton?
if let recentDocuments = recentDocuments, recentDocuments.count > 10 {
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
expandImpl?()
updateState { state in
var updatedState = state
updatedState.searching = true
return updatedState
}
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Attachment_File), leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
}), rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments), style: .blocks, emptyStateItem: nil, animateChanges: false)
}), rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
var emptyItem: AttachmentFileEmptyStateItem?
if let recentDocuments = recentDocuments, recentDocuments.isEmpty {
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings)
}
var searchItem: ItemListControllerSearch?
if state.searching {
searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, cancel: {
updateState { state in
var updatedState = state
updatedState.searching = false
return updatedState
}
}, send: { message in
arguments.send(message)
}, dismissInput: {
dismissInputImpl?()
})
}
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
} |> afterDisposed {
@ -182,8 +279,21 @@ public func attachmentFileController(context: AccountContext, presentGallery: @e
}
let controller = AttachmentFileControllerImpl(context: context, state: signal)
controller.prepareForReuseImpl = {
updateState { state in
var updatedState = state
updatedState.searching = false
return updatedState
}
}
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
expandImpl = { [weak controller] in
controller?.requestAttachmentMenuExpansion()
}
return controller
}

View file

@ -0,0 +1,103 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
final class AttachmentFileEmptyStateItem: ItemListControllerEmptyStateItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
self.context = context
self.theme = theme
self.strings = strings
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let item = to as? AttachmentFileEmptyStateItem {
return self.theme === item.theme && self.strings === item.strings
} else {
return false
}
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? AttachmentFileEmptyStateItemNode {
current.item = self
return current
} else {
return AttachmentFileEmptyStateItemNode(item: self)
}
}
}
final class AttachmentFileEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private var animationNode: AnimatedStickerNode
private let textNode: ASTextNode
private var validLayout: (ContainerViewLayout, CGFloat)?
var item: AttachmentFileEmptyStateItem {
didSet {
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if let (layout, navigationHeight) = self.validLayout {
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
}
init(item: AttachmentFileEmptyStateItem) {
self.item = item
self.animationNode = AnimatedStickerNode()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Files"), width: 320, height: 320, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.textNode.attributedText = NSAttributedString(string: strings.Attachment_FilesIntro, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [])
insets.top += navigationBarHeight - 92.0
let imageSpacing: CGFloat = 12.0
let imageSize = CGSize(width: 144.0, height: 144.0)
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
self.animationNode.updateLayout(size: imageSize)
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + textSize.height
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight), size: textSize))
}
}

View file

@ -0,0 +1,566 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import ItemListUI
import PresentationDataUtils
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import SearchBarNode
import MergeLists
import ChatListSearchItemHeader
import ItemListUI
import SearchUI
import ContextUI
import ListMessageItem
private let searchBarFont = Font.regular(17.0)
private final class AttachmentFileSearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode {
private var theme: PresentationTheme
private let strings: PresentationStrings
private let cancel: () -> Void
private let searchBar: SearchBarNode
private var queryUpdated: ((String) -> Void)?
var activity: Bool = false {
didSet {
self.searchBar.activity = activity
}
}
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) {
self.theme = theme
self.strings = strings
self.cancel = cancel
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false)
super.init()
self.addSubnode(self.searchBar)
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.cancel()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.queryUpdated?(query)
}
updateActivity({ [weak self] value in
self?.activity = value
})
self.updatePlaceholder()
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {
self.queryUpdated = f
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: self.theme), strings: self.strings)
self.updatePlaceholder()
}
func updatePlaceholder() {
self.searchBar.placeholderString = NSAttributedString(string: self.strings.Attachment_FilesSearchPlaceholder, font: searchBarFont, textColor: self.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
override var nominalHeight: CGFloat {
return 56.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
}
final class AttachmentFileSearchItem: ItemListControllerSearch {
let context: AccountContext
let presentationData: PresentationData
let cancel: () -> Void
let send: (Message) -> Void
let dismissInput: () -> Void
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.presentationData = presentationData
self.cancel = cancel
self.send = send
self.dismissInput = dismissInput
self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
if value {
return .single(value) |> delay(0.2, queue: Queue.mainQueue())
} else {
return .single(value)
}
}).start(next: { [weak self] value in
self?.updateActivity?(value)
}))
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? AttachmentFileSearchItem {
if self.context !== to.context {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode {
let presentationData = self.presentationData
if let current = current as? AttachmentFileSearchNavigationContentNode {
current.updateTheme(presentationData.theme)
return current
} else {
return AttachmentFileSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel, updateActivity: { [weak self] value in
self?.updateActivity = value
})
}
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
return AttachmentFileSearchItemNode(context: self.context, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
self?.activity.set(value)
}, dismissInput: self.dismissInput)
}
}
private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private let containerNode: AttachmentFileSearchContainerNode
init(context: AccountContext, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
self.containerNode = AttachmentFileSearchContainerNode(context: context, forceTheme: nil, send: { message in
send(message)
}, updateActivity: updateActivity)
self.containerNode.cancel = {
cancel()
}
super.init()
self.addSubnode(self.containerNode)
self.containerNode.dismissInput = {
dismissInput()
}
}
override func queryUpdated(_ query: String) {
self.containerNode.searchTextUpdated(text: query)
}
override func scrollToTop() {
self.containerNode.scrollToTop()
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)))
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
private final class AttachmentFileSearchContainerInteraction {
let context: AccountContext
let send: (Message) -> Void
init(context: AccountContext, send: @escaping (Message) -> Void) {
self.context = context
self.send = send
}
}
private enum AttachmentFileSearchEntryId: Hashable {
case placeholder(Int)
case message(MessageId)
}
private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool {
guard let lhsMessage = lhsMessage, let rhsMessage = rhsMessage else {
return lhsMessage == nil && rhsMessage == nil
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
private final class AttachmentFileSearchEntry: Comparable, Identifiable {
let index: Int
let message: Message?
init(index: Int, message: Message?) {
self.index = index
self.message = message
}
var stableId: AttachmentFileSearchEntryId {
if let message = self.message {
return .message(message.id)
} else {
return .placeholder(self.index)
}
}
static func ==(lhs: AttachmentFileSearchEntry, rhs: AttachmentFileSearchEntry) -> Bool {
return lhs.index == rhs.index && areMessagesEqual(lhs.message, rhs.message)
}
static func <(lhs: AttachmentFileSearchEntry, rhs: AttachmentFileSearchEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction) -> ListViewItem {
let itemInteraction = ListMessageItemInteraction(openMessage: { message, _ in
interaction.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
return ListMessageItem(presentationData: ChatPresentationData(presentationData: interaction.context.sharedContext.currentPresentationData.with({$0})), context: interaction.context, chatLocation: .peer(PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
}
}
struct AttachmentFileSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
let isEmpty: Bool
let query: String
}
private func attachmentFileSearchContainerPreparedRecentTransition(from fromEntries: [AttachmentFileSearchEntry], to toEntries: [AttachmentFileSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction) -> AttachmentFileSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
return AttachmentFileSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query)
}
public final class AttachmentFileSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let send: (Message) -> Void
private let dimNode: ASDisplayNode
private let listNode: ListView
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private var enqueuedTransitions: [(AttachmentFileSearchContainerTransition, Bool)] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private let searchQuery = Promise<String?>()
private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
private var _hasDim: Bool = false
override public var hasDim: Bool {
return _hasDim
}
public init(context: AccountContext, forceTheme: PresentationTheme?, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
self.context = context
self.send = send
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.forceTheme = forceTheme
if let forceTheme = self.forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.emptyResultsTitleNode = ImmediateTextNode()
self.emptyResultsTitleNode.displaysAsynchronously = false
self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor)
self.emptyResultsTitleNode.textAlignment = .center
self.emptyResultsTitleNode.isHidden = true
self.emptyResultsTextNode = ImmediateTextNode()
self.emptyResultsTextNode.displaysAsynchronously = false
self.emptyResultsTextNode.maximumNumberOfLines = 0
self.emptyResultsTextNode.textAlignment = .center
self.emptyResultsTextNode.isHidden = true
super.init()
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
self._hasDim = true
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
self.addSubnode(self.emptyResultsTitleNode)
self.addSubnode(self.emptyResultsTextNode)
let interaction = AttachmentFileSearchContainerInteraction(context: context, send: { [weak self] message in
send(message)
self?.listNode.clearHighlightAnimated(true)
})
let presentationDataPromise = self.presentationDataPromise
let searchQuery = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> in
if let query = query, !query.isEmpty {
return (.complete() |> delay(0.6, queue: Queue.mainQueue()))
|> then(.single(query))
} else {
return .single(query)
}
}
let foundItems = searchQuery
|> mapToSignal { query -> Signal<[AttachmentFileSearchEntry]?, NoError> in
guard let query = query, !query.isEmpty else {
return .single(nil)
}
let signal: Signal<[Message]?, NoError> = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
updateActivity(true)
return combineLatest(signal, presentationDataPromise.get())
|> mapToSignal { messages, presentationData -> Signal<[AttachmentFileSearchEntry]?, NoError> in
var entries: [AttachmentFileSearchEntry] = []
var index = 0
if let messages = messages {
for message in messages {
entries.append(AttachmentFileSearchEntry(index: index, message: message))
index += 1
}
} else {
for _ in 0 ..< 2 {
entries.append(AttachmentFileSearchEntry(index: index, message: nil))
index += 1
}
}
return .single(entries)
}
}
let previousSearchItems = Atomic<[AttachmentFileSearchEntry]?>(value: nil)
self.searchDisposable.set((combineLatest(searchQuery, foundItems, self.presentationDataPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] query, entries, presentationData in
if let strongSelf = self {
let previousEntries = previousSearchItems.swap(entries)
updateActivity(false)
let firstTime = previousEntries == nil
let transition = attachmentFileSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
var presentationData = presentationData
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
if let forceTheme = strongSelf.forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
override public func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override public func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: AttachmentFileSearchContainerTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.listNode.isHidden = !isSearching
strongSelf.dimNode.isHidden = transition.isSearching
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
let emptyResults = transition.isSearching && transition.isEmpty
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
})
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let hadValidLayout = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let padding: CGFloat = 16.0
let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSpacing: CGFloat = 8.0
let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing
let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0)
transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize))
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
override public func scrollToTop() {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = self.view.hitTest(point, with: event) else {
return nil
}
if result === self.view {
return nil
}
return result
}
}

View file

@ -70,6 +70,7 @@ import LottieMeshSwift
import ReactionListContextMenuContent
import AttachmentUI
import AttachmentTextInputPanelNode
import MediaPickerUI
import ChatPresentationInterfaceState
import Pasteboard
import ChatSendMessageActionUI
@ -459,6 +460,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
private weak var attachmentController: AttachmentController?
private weak var currentImportMessageTooltip: UndoOverlayController?
public override var customData: Any? {
@ -583,7 +586,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return
}
strongSelf.chatDisplayNode.scrollToTop()
if let attachmentController = strongSelf.attachmentController {
attachmentController.scrollToTop?()
} else {
strongSelf.chatDisplayNode.scrollToTop()
}
}
self.attemptNavigation = { [weak self] action in
@ -3166,7 +3173,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
} else {
strongSelf.presentMediaPicker(fileMode: false, editingMedia: true, present: { [weak self] c, _ in
strongSelf.presentOldMediaPicker(fileMode: false, editingMedia: true, present: { [weak self] c, _ in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { signals, _, _ in
self?.interfaceInteraction?.setupEditMessage(messageId, { _ in })
@ -3587,6 +3594,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
title = presentationInterfaceState.strings.ReportPeer_ReasonChildAbuse
case .copyright:
title = presentationInterfaceState.strings.ReportPeer_ReasonCopyright
case .illegalDrugs:
title = presentationInterfaceState.strings.ReportPeer_ReasonIllegalDrugs
case .personalDetails:
title = presentationInterfaceState.strings.ReportPeer_ReasonPersonalDetails
case .custom:
title = presentationInterfaceState.strings.ReportPeer_ReasonOther
case .irrelevantLocation:
@ -6203,7 +6214,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, reportMessages: { [weak self] messages, contextController in
if let strongSelf = self, !messages.isEmpty {
presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), completion: { _, _ in })
let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in })
}
}, blockMessageAuthor: { [weak self] message, contextController in
contextController?.dismiss(completion: {
@ -10228,66 +10240,78 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return inputPanelNode
}
private func openCamera() {
private func openCamera(cameraView: TGAttachmentCameraView? = nil) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
var photoOnly = false
if let callManager = self.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
photoOnly = true
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
return entry ?? GeneratedMediaStoreSettings.defaultSettings
}
let storeEditedPhotos = false
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText
presentedLegacyCamera(context: self.context, peer: peer, chatLocation: self.chatLocation, cameraView: nil, menuController: nil, parentController: self, editingMedia: false, saveCapturedPhotos: storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: self.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
// if editMediaOptions != nil {
// strongSelf.editMessageMediaWithLegacySignals(signals!)
// } else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
// }
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
|> deliverOnMainQueue).start(next: { [weak self] settings in
guard let strongSelf = self else {
return
}
}, recognizedQRCode: { [weak self] code in
if let strongSelf = self {
if let (host, port, username, password, secret) = parseProxyUrl(code) {
strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil)
}
var photoOnly = false
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
photoOnly = true
}
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
let storeEditedPhotos = settings.storeEditedPhotos
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, editingMedia: false, saveCapturedPhotos: storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
// if editMediaOptions != nil {
// strongSelf.editMessageMediaWithLegacySignals(signals!)
// } else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
// }
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, presentStickers: { [weak self] completion in
if let strongSelf = self {
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect)
return true
})
strongSelf.present(controller, in: .window(.root))
return controller
} else {
return nil
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView()
}
}, recognizedQRCode: { [weak self] code in
if let strongSelf = self {
if let (host, port, username, password, secret) = parseProxyUrl(code) {
strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil)
}
}
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, presentStickers: { [weak self] completion in
if let strongSelf = self {
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect)
return true
})
strongSelf.present(controller, in: .window(.root))
return controller
} else {
return nil
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView()
}, dismissedWithResult: { [weak self] in
self?.attachmentController?.dismiss(animated: false, completion: nil)
})
})
}
@ -10297,10 +10321,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
self.chatDisplayNode.dismissInput()
let currentLocationController = Atomic<LocationPickerController?>(value: nil)
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
let currentLocationController = Atomic<AttachmentContainable?>(value: nil)
var canSendPolls = true
if let _ = peer as? TelegramUser {
if peer is TelegramUser || peer is TelegramSecretChat {
canSendPolls = false
} else if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendPolls) != nil {
@ -10312,44 +10337,59 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
var buttons: [AttachmentButtonType] = [.camera, .gallery, .file, .location, .contact]
var availableTabs: [AttachmentButtonType] = [.gallery, .file, .location, .contact]
if canSendPolls {
buttons.append(.poll)
availableTabs.append(.poll)
}
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText
let attachmentController = AttachmentController(context: self.context, buttons: buttons)
let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, buttons: availableTabs)
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
guard let strongSelf = self else {
return
}
switch type {
case .camera:
completion(nil, nil)
attachmentController?.dismiss(animated: true)
strongSelf.openCamera()
strongSelf.controllerNavigationDisposable.set(nil)
case .gallery:
strongSelf.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { controller, mediaPickerContext in
strongSelf.presentMediaPicker(present: { controller, mediaPickerContext in
completion(controller, mediaPickerContext)
}, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in
attachmentController?.mediaPickerContext = mediaPickerContext
}, completion: { [weak self] signals, silentPosting, scheduleTime in
if !inputText.string.isEmpty {
self?.clearInputText()
}
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime)
})
strongSelf.controllerNavigationDisposable.set(nil)
case .file:
let controller = attachmentFileController(context: strongSelf.context, presentGallery: { [weak self, weak attachmentController] in
strongSelf.controllerNavigationDisposable.set(nil)
let existingController = currentFilesController.with { $0 }
if let controller = existingController {
completion(controller, nil)
return
}
let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, presentGallery: { [weak self, weak attachmentController] in
attachmentController?.dismiss(animated: true)
self?.presentFileGallery()
}, presentFiles: { [weak self, weak attachmentController] in
attachmentController?.dismiss(animated: true)
self?.presentICloudFileGallery()
}, send: { [weak self] mediaReference in
guard let strongSelf = self else {
return
}
let peerId = strongSelf.chatLocation.peerId
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message]))
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
})
})
let _ = currentFilesController.swap(controller)
completion(controller, nil)
strongSelf.controllerNavigationDisposable.set(nil)
case .location:
strongSelf.controllerNavigationDisposable.set(nil)
let existingController = currentLocationController.with { $0 }
@ -10394,11 +10434,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
case .contact:
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true))
contactsController.presentScheduleTimePicker = { [weak self] completion in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(completion: completion)
}
}
contactsController.navigationPresentation = .modal
completion(contactsController, nil)
completion(contactsController, contactsController.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self, let (peers, _) = peers {
if let strongSelf = self, let (peers, _, silent, scheduleTime) = peers {
if peers.count > 1 {
var enqueueMessages: [EnqueueMessage] = []
for peer in peers {
@ -10435,7 +10480,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
enqueueMessages.append(message)
}
}
strongSelf.sendMessages(enqueueMessages)
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
} else if let peer = peers.first {
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
switch peer {
@ -10491,7 +10536,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.sendMessages([message])
strongSelf.sendMessages(strongSelf.transformEnqueueMessages([message], silentPosting: silent, scheduleTime: scheduleTime))
} else {
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
@ -10509,7 +10554,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.sendMessages([message])
strongSelf.sendMessages(strongSelf.transformEnqueueMessages([message], silentPosting: silent, scheduleTime: scheduleTime))
}
}), completed: nil, cancelled: nil)
strongSelf.effectiveNavigationController?.pushViewController(contactController)
@ -10528,6 +10573,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
self.present(attachmentController, in: .window(.root))
self.attachmentController = attachmentController
}
private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) {
@ -10628,7 +10674,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: {
self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { [weak self] c, _ in
self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { [weak self] c, _ in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { signals, silentPosting, scheduleTime in
if !inputText.string.isEmpty {
@ -10698,8 +10744,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, openFileGallery: {
self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil)
}, openWebSearch: {
self?.presentWebSearch(editingMessage : editMediaOptions != nil)
}, openWebSearch: { [weak self] in
self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}, openMap: {
self?.presentLocationPicker()
}, openContacts: {
@ -10814,7 +10862,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
private func presentFileGallery(editingMessage: Bool = false) {
self.presentMediaPicker(fileMode: true, editingMedia: editingMessage, present: { [weak self] c, _ in
self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, present: { [weak self] c, _ in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { [weak self] signals, silentPosting, scheduleTime in
if editingMessage {
@ -10930,7 +10978,68 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(actionSheet, in: .window(.root))
}
private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) {
private func presentMediaPicker(present: @escaping (AttachmentContainable, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?) -> Void) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), chatLocation: self.chatLocation)
let mediaPickerContext = controller.mediaPickerContext
controller.openCamera = { [weak self] cameraView in
self?.openCamera(cameraView: cameraView)
}
controller.presentWebSearch = { [weak self, weak controller] in
self?.presentWebSearch(editingMessage: false, attachment: true, present: { [weak controller] c, a in
controller?.present(c, in: .current)
if let webSearchController = c as? WebSearchController {
webSearchController.dismissed = {
updateMediaPickerContext(mediaPickerContext)
}
controller?.webSearchController = webSearchController
updateMediaPickerContext(webSearchController.mediaPickerContext)
}
})
}
controller.presentStickers = { [weak self] completion in
if let strongSelf = self {
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect)
return true
})
strongSelf.present(controller, in: .window(.root))
return controller
} else {
return nil
}
}
controller.presentSchedulePicker = { [weak self] media, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}
controller.presentTimerPicker = { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}
controller.getCaptionPanelView = { [weak self] in
return self?.getCaptionPanelView()
}
controller.legacyCompletion = { signals, silently, scheduleTime in
completion(signals, silently, scheduleTime)
}
present(controller, mediaPickerContext)
}
private func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) {
let postbox = self.context.account.postbox
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, SearchBotsConfiguration), NoError> in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
@ -10968,7 +11077,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in
if let strongSelf = self {
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(completion: { results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in
if let legacyController = legacyController {
legacyController.dismiss()
}
@ -11066,7 +11175,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func presentWebSearch(editingMessage: Bool) {
private func presentWebSearch(editingMessage: Bool, attachment: Bool, present: @escaping (ViewController, Any?) -> Void) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
@ -11080,7 +11189,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
|> deliverOnMainQueue).start(next: { [weak self] configuration in
if let strongSelf = self {
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(completion: { [weak self] results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in
self?.attachmentController?.dismiss(animated: true, completion: nil)
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in
if let strongSelf = self {
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
@ -11110,7 +11220,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
controller.getCaptionPanelView = { [weak self] in
return self?.getCaptionPanelView()
}
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
})
}
@ -11162,7 +11272,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.effectiveNavigationController?.pushViewController(contactsController)
self.controllerNavigationDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self, let (peers, _) = peers {
if let strongSelf = self, let (peers, _, _, _) = peers {
if peers.count > 1 {
var enqueueMessages: [EnqueueMessage] = []
for peer in peers {

View file

@ -157,7 +157,7 @@ public class ComposeControllerImpl: ViewController, ComposeController {
strongSelf.createActionDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
if let strongSelf = self, let (contactPeers, _) = result, case let .peer(peer, _, _) = contactPeers.first {
if let strongSelf = self, let (contactPeers, _, _, _) = result, case let .peer(peer, _, _) = contactPeers.first {
controller?.dismissSearch()
controller?.displayNavigationActivity = true
strongSelf.createActionDisposable.set((strongSelf.context.engine.peers.createSecretChat(peerId: peer.id) |> deliverOnMainQueue).start(next: { peerId in

View file

@ -11,12 +11,13 @@ import AccountContext
import ContactListUI
import SearchUI
import AttachmentUI
import SearchBarNode
class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable {
private let context: AccountContext
private let autoDismiss: Bool
private var contactsNode: ContactSelectionControllerNode {
fileprivate var contactsNode: ContactSelectionControllerNode {
return self.displayNode as! ContactSelectionControllerNode
}
@ -42,21 +43,23 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
return self._ready
}
private let _result = Promise<([ContactListPeer], ContactListAction)?>()
var result: Signal<([ContactListPeer], ContactListAction)?, NoError> {
private let _result = Promise<([ContactListPeer], ContactListAction, Bool, Int32?)?>()
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?)?, NoError> {
return self._result.get()
}
private let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
var dismissed: (() -> Void)?
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in }
private let createActionDisposable = MetaDisposable()
private let confirmationDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var searchContentNode: NavigationBarSearchContentNode?
private var searchContentNode: NavigationBarContentNode?
var displayNavigationActivity: Bool = false {
didSet {
@ -98,10 +101,10 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
if let searchContentNode = strongSelf.searchContentNode as? NavigationBarSearchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.contactsNode.contactListNode.scrollToTop()
strongSelf.contactsNode.scrollToTop()
}
}
@ -127,7 +130,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
}
if params.multipleSelection {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.beginSelection))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.beginSearch))
}
}
@ -140,6 +143,11 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.presentationDataDisposable?.dispose()
}
@objc private func beginSearch() {
self.requestAttachmentMenuExpansion()
self.activateSearch()
}
@objc private func beginSelection() {
self.navigationItem.rightBarButtonItem = nil
self.contactsNode.beginSelection()
@ -148,7 +156,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
(self.searchContentNode as? NavigationBarSearchContentNode)?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
self.title = self.titleProducer(self.presentationData.strings)
self.tabBarItem.title = self.presentationData.strings.Contacts_Title
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
@ -196,23 +204,23 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
}
self.contactsNode.contactListNode.contentOffsetChanged = { [weak self] offset in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode as? NavigationBarSearchContentNode {
searchContentNode.updateListVisibleContentOffset(offset)
}
}
self.contactsNode.contactListNode.contentScrollingEnded = { [weak self] listView in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode as? NavigationBarSearchContentNode {
return fixNavigationSearchableListNodeScrolling(listView, searchNode: searchContentNode)
} else {
return false
}
}
self.contactsNode.requestMultipleAction = { [weak self] in
self.contactsNode.requestMultipleAction = { [weak self] silent, scheduleTime in
if let strongSelf = self {
let selectedPeers = strongSelf.contactsNode.contactListNode.selectedPeers
strongSelf._result.set(.single((selectedPeers, .generic)))
strongSelf._result.set(.single((selectedPeers, .generic, silent, scheduleTime)))
if strongSelf.autoDismiss {
strongSelf.dismiss()
}
@ -268,10 +276,27 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
private func activateSearch() {
if self.displayNavigationBar {
if let searchContentNode = self.searchContentNode {
if let searchContentNode = self.searchContentNode as? NavigationBarSearchContentNode {
self.contactsNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
} else if self.multipleSelection {
let contentNode = ContactsSearchNavigationContentNode(presentationData: self.presentationData, dismissSearch: { [weak self] in
if let strongSelf = self, let navigationBar = strongSelf.navigationBar, let searchContentNode = strongSelf.searchContentNode as? ContactsSearchNavigationContentNode {
searchContentNode.deactivate()
strongSelf.searchContentNode = nil
navigationBar.setContentNode(nil, animated: true)
strongSelf.contactsNode.deactivateOverlaySearch()
}
}, updateSearchQuery: { [weak self] query in
if let strongSelf = self {
strongSelf.contactsNode.searchContainerNode?.searchTextUpdated(text: query)
}
})
self.searchContentNode = contentNode
self.navigationBar?.setContentNode(contentNode, animated: true)
self.contactsNode.activateOverlaySearch()
contentNode.activate()
}
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
@ -279,7 +304,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
if !self.displayNavigationBar {
self.contactsNode.prepareDeactivateSearch()
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
if let searchContentNode = self.searchContentNode {
if let searchContentNode = self.searchContentNode as? NavigationBarSearchContentNode {
self.contactsNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
}
}
@ -290,7 +315,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf._result.set(.single(([peer], action)))
strongSelf._result.set(.single(([peer], action, false, nil)))
if strongSelf.autoDismiss {
strongSelf.dismiss()
}
@ -302,4 +327,99 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
func dismissSearch() {
self.deactivateSearch()
}
public var mediaPickerContext: AttachmentMediaPickerContext {
return ContactsPickerContext(controller: self)
}
}
private let searchBarFont = Font.regular(17.0)
final class ContactsSearchNavigationContentNode: NavigationBarContentNode {
private var presentationData: PresentationData
private let searchBar: SearchBarNode
init(presentationData: PresentationData, dismissSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void) {
self.presentationData = presentationData
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings, fieldStyle: .modern)
self.searchBar.placeholderString = NSAttributedString(string: presentationData.strings.Common_Search, font: searchBarFont, textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
super.init()
self.addSubnode(self.searchBar)
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
dismissSearch()
}
self.searchBar.textUpdated = { query, _ in
updateSearchQuery(query)
}
}
override var nominalHeight: CGFloat {
return 56.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
func updateActivity(_ activity: Bool) {
self.searchBar.activity = activity
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings)
}
}
final class ContactsPickerContext: AttachmentMediaPickerContext {
private weak var controller: ContactSelectionControllerImpl?
var selectionCount: Signal<Int, NoError> {
if let controller = self.controller {
return controller.contactsNode.contactListNode.selectionStateSignal
|> map { state in
return state?.selectedPeerIndices.count ?? 0
}
} else {
return .single(0)
}
}
var caption: Signal<NSAttributedString?, NoError> {
return .single(nil)
}
init(controller: ContactSelectionControllerImpl) {
self.controller = controller
}
func setCaption(_ caption: NSAttributedString) {
}
func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
self.controller?.contactsNode.requestMultipleAction?(silently, nil)
}
func schedule() {
self.controller?.presentScheduleTimePicker ({ time in
self.controller?.contactsNode.requestMultipleAction?(false, time)
})
}
}

View file

@ -36,7 +36,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)?
var requestMultipleAction: (() -> Void)?
var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?) -> Void)?
var dismiss: (() -> Void)?
var presentationData: PresentationData {
@ -50,13 +50,18 @@ final class ContactSelectionControllerNode: ASDisplayNode {
private var selectionState: ContactListNodeGroupSelectionState?
var searchContainerNode: ContactsSearchContainerNode?
init(context: AccountContext, presentationData: PresentationData, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool) {
self.context = context
self.presentationData = presentationData
self.displayDeviceContacts = displayDeviceContacts
self.displayCallIcons = displayCallIcons
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, multipleSelection: multipleSelection)
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?) -> Void)?
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture in
contextActionImpl?(peer, node, gesture)
} : nil, multipleSelection: multipleSelection)
self.dimNode = ASDisplayNode()
@ -96,7 +101,17 @@ final class ContactSelectionControllerNode: ASDisplayNode {
}
shareImpl = { [weak self] in
self?.requestMultipleAction?()
self?.requestMultipleAction?(false, nil)
}
contextActionImpl = { [weak self] peer, node, gesture in
if let strongSelf = self, (strongSelf.selectionState?.selectedPeerIndices.isEmpty ?? true) {
strongSelf.contactListNode.updateSelectionState { state in
let peerId = ContactListPeerId.peer(peer.id)
let state = state ?? ContactListNodeGroupSelectionState()
return state.withToggledPeerId(peerId).withSelectedPeerMap([peerId: ContactListPeer.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)])
}
}
}
}
@ -132,17 +147,91 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
let countPanelHeight = self.countPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
if (self.selectionState?.selectedPeerIndices.isEmpty ?? true) {
transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: countPanelHeight)))
} else {
insets.bottom += countPanelHeight
transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - countPanelHeight), size: CGSize(width: layout.size.width, height: countPanelHeight)))
}
// let countPanelHeight = self.countPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
// if (self.selectionState?.selectedPeerIndices.isEmpty ?? true) {
// transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: countPanelHeight)))
// } else {
// insets.bottom += countPanelHeight
// transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - countPanelHeight), size: CGSize(width: layout.size.width, height: countPanelHeight)))
// }
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
if let searchContainerNode = self.searchContainerNode {
searchContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
searchContainerNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition)
}
}
func scrollToTop() {
if let searchContainerNode = self.searchContainerNode {
searchContainerNode.scrollToTop()
} else {
self.contactListNode.scrollToTop()
}
}
func activateOverlaySearch() {
guard let (containerLayout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
return
}
var categories: ContactsSearchCategories = [.cloudContacts]
if self.displayDeviceContacts {
categories.insert(.deviceContacts)
} else {
categories.insert(.global)
}
let searchContainerNode = ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in
if let strongSelf = self {
var updated = false
strongSelf.contactListNode.updateSelectionState { state -> ContactListNodeGroupSelectionState? in
if let state = state {
updated = true
var foundPeers = state.foundPeers
var selectedPeerMap = state.selectedPeerMap
selectedPeerMap[peer.id] = peer
var exists = false
for foundPeer in foundPeers {
if peer.id == foundPeer.id {
exists = true
break
}
}
if !exists {
foundPeers.insert(peer, at: 0)
}
return state.withToggledPeerId(peer.id).withFoundPeers(foundPeers).withSelectedPeerMap(selectedPeerMap)
} else {
return nil
}
}
if updated {
strongSelf.requestDeactivateSearch?()
} else {
strongSelf.requestOpenPeerFromSearch?(peer)
}
}
}, contextAction: nil)
self.insertSubnode(searchContainerNode, belowSubnode: navigationBar)
self.searchContainerNode = searchContainerNode
searchContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate)
}
func deactivateOverlaySearch() {
guard let searchContainerNode = self.searchContainerNode else {
return
}
searchContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchContainerNode] _ in
searchContainerNode?.removeFromSupernode()
})
self.searchContainerNode = nil
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {

View file

@ -10,7 +10,7 @@ import ShareController
import LegacyUI
import LegacyMediaPickerUI
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: String, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) {
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: String, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -93,8 +93,12 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
let screenSize = parentController.view.bounds.size
var startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height)
if let cameraView = cameraView, let menuController = menuController {
startFrame = menuController.view.convert(cameraView.previewView()!.frame, from: cameraView)
if let cameraView = cameraView {
if let menuController = menuController {
startFrame = menuController.view.convert(cameraView.previewView()!.frame, from: cameraView)
} else {
startFrame = parentController.view.convert(cameraView.previewView()!.frame, from: cameraView)
}
}
legacyController.bind(controller: controller)
@ -133,6 +137,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
menuController?.dismiss(animated: false)
legacyController?.dismissWithAnimation()
dismissedWithResult()
}
controller.finishedWithPhoto = { [weak menuController, weak legacyController] overlayController, image, caption, stickers, timer in
@ -150,6 +155,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
menuController?.dismiss(animated: false)
legacyController?.dismissWithAnimation()
dismissedWithResult()
}
controller.finishedWithVideo = { [weak menuController, weak legacyController] overlayController, videoURL, previewImage, duration, dimensions, adjustments, caption, stickers, timer in
@ -174,6 +180,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
}
menuController?.dismiss(animated: false)
legacyController?.dismissWithAnimation()
dismissedWithResult()
}
controller.recognizedQRCode = { code in

View file

@ -8059,7 +8059,7 @@ func presentAddMembers(context: AccountContext, updatedPresentationData: (initia
if let contactsController = contactsController as? ContactSelectionController {
selectAddMemberDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak contactsController] result in
guard let (peers, _) = result, let memberPeer = peers.first else {
guard let (peers, _, _, _) = result, let memberPeer = peers.first else {
return
}

View file

@ -452,9 +452,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.searchDisplayController?.updatePresentationData(self.presentationData)
self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) })
self.toolbarBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode?.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme))
if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {

View file

@ -31,6 +31,7 @@ swift_library(
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/AppBundle:AppBundle",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AttachmentUI:AttachmentUI",
],
visibility = [
"//visibility:public",

View file

@ -8,6 +8,7 @@ import LegacyComponents
import TelegramUIPreferences
import TelegramPresentationData
import AccountContext
import AttachmentUI
public func requestContextResults(context: AccountContext, botId: EnginePeer.Id, query: String, peerId: EnginePeer.Id, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal<RequestChatContextResultsResult?, NoError> {
return context.engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
@ -60,7 +61,7 @@ public enum WebSearchMode {
}
public enum WebSearchControllerMode {
case media(completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool) -> Void)
case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool) -> Void)
case avatar(initialQuery: String?, completion: (UIImage) -> Void)
var mode: WebSearchMode {
@ -161,6 +162,8 @@ public final class WebSearchController: ViewController {
}
}
public var dismissed: () -> Void = { }
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: SearchBotsConfiguration, mode: WebSearchControllerMode) {
self.context = context
self.mode = mode
@ -225,13 +228,22 @@ public final class WebSearchController: ViewController {
}
})
let navigationContentNode = WebSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings)
var attachment = false
if case let .media(attachmentValue, _) = mode {
attachment = attachmentValue
}
let navigationContentNode = WebSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, attachment: attachment)
self.navigationContentNode = navigationContentNode
navigationContentNode.setQueryUpdated { [weak self] query in
if let strongSelf = self, strongSelf.isNodeLoaded {
strongSelf.updateSearchQuery(query)
}
}
navigationContentNode.cancel = { [weak self] in
if let strongSelf = self {
strongSelf.cancel()
}
}
self.navigationBar?.setContentNode(navigationContentNode, animated: false)
if let query = searchQuery {
navigationContentNode.setQuery(query)
@ -274,7 +286,7 @@ public final class WebSearchController: ViewController {
let currentItem = LegacyWebSearchItem(result: current)
selectionState.setItem(currentItem, selected: true)
}
if case let .media(sendSelected) = mode {
if case let .media(_, sendSelected) = mode {
sendSelected(results, selectionState, editingState, false)
}
}
@ -304,6 +316,14 @@ public final class WebSearchController: ViewController {
self.selectionDisposable?.dispose()
}
public func cancel() {
self.controllerNode.dismissInput?()
self.controllerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
self?.dismissed()
self?.dismiss()
})
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@ -322,11 +342,19 @@ public final class WebSearchController: ViewController {
if case let .avatar(initialQuery, _) = mode, let _ = initialQuery {
select = true
}
if case let .media(attachment, _) = mode, attachment && !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.navigationContentNode?.activate(select: select)
}
override public func loadDisplayNode() {
self.displayNode = WebSearchControllerNode(context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode)
var attachment: Bool = false
if case let .media(attachmentValue, _) = self.mode, attachmentValue {
attachment = true
}
self.displayNode = WebSearchControllerNode(context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode, attachment: attachment)
self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in
if let strongSelf = self {
strongSelf.updateInterfaceState(f)
@ -501,6 +529,14 @@ public final class WebSearchController: ViewController {
}
}
public var mediaPickerContext: WebSearchPickerContext? {
if let interaction = self.controllerInteraction {
return WebSearchPickerContext(interaction: interaction)
} else {
return nil
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
@ -509,3 +545,49 @@ public final class WebSearchController: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
public class WebSearchPickerContext: AttachmentMediaPickerContext {
private weak var interaction: WebSearchControllerInteraction?
public var selectionCount: Signal<Int, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
public var caption: Signal<NSAttributedString?, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in
if let caption = caption as? NSAttributedString {
subscriber.putNext(caption)
} else {
subscriber.putNext(nil)
}
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
init(interaction: WebSearchControllerInteraction) {
self.interaction = interaction
}
public func setCaption(_ caption: NSAttributedString) {
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
}
public func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
// self.interaction?.sendSelected(nil, silently, nil, true)
}
public func schedule() {
// self.interaction?.schedule()
}
}

View file

@ -64,8 +64,8 @@ private func preparedTransition(from fromEntries: [WebSearchEntry], to toEntries
return WebSearchTransition(deleteItems: deleteIndices, insertItems: insertions, updateItems: updates, entryCount: toEntries.count, hasMore: hasMore)
}
private func gridNodeLayoutForContainerLayout(size: CGSize) -> GridNodeLayoutType {
let side = floorToScreenPixels((size.width - 3.0) / 4.0)
private func gridNodeLayoutForContainerLayout(size: CGSize, insets: UIEdgeInsets) -> GridNodeLayoutType {
let side = floorToScreenPixels((size.width - insets.left - insets.right - 2.0) / 3.0)
return .fixed(itemSize: CGSize(width: side, height: side), fillWidth: true, lineSpacing: 1.0, itemSpacing: 1.0)
}
@ -123,6 +123,7 @@ class WebSearchControllerNode: ASDisplayNode {
private var strings: PresentationStrings
private var presentationData: PresentationData
private let mode: WebSearchMode
private let attachment: Bool
private let controllerInteraction: WebSearchControllerInteraction
private var webSearchInterfaceState: WebSearchInterfaceState
@ -172,7 +173,7 @@ class WebSearchControllerNode: ASDisplayNode {
var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?
var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode) {
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) {
self.context = context
self.theme = presentationData.theme
self.strings = presentationData.strings
@ -181,6 +182,7 @@ class WebSearchControllerNode: ASDisplayNode {
self.peer = peer
self.chatLocation = chatLocation
self.mode = mode
self.attachment = attachment
self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 })
self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true)
@ -220,18 +222,22 @@ class WebSearchControllerNode: ASDisplayNode {
})
self.addSubnode(self.gridNode)
self.addSubnode(self.recentQueriesNode)
if !attachment {
self.addSubnode(self.recentQueriesNode)
}
self.addSubnode(self.segmentedBackgroundNode)
self.addSubnode(self.segmentedSeparatorNode)
if case .media = mode {
self.addSubnode(self.segmentedControlNode)
}
self.addSubnode(self.toolbarBackgroundNode)
self.addSubnode(self.toolbarSeparatorNode)
self.addSubnode(self.cancelButton)
self.addSubnode(self.sendButton)
self.addSubnode(self.attributionNode)
self.addSubnode(self.badgeNode)
if !attachment {
self.addSubnode(self.toolbarBackgroundNode)
self.addSubnode(self.toolbarSeparatorNode)
self.addSubnode(self.cancelButton)
self.addSubnode(self.sendButton)
self.addSubnode(self.attributionNode)
self.addSubnode(self.badgeNode)
}
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
if let strongSelf = self, let scope = WebSearchScope(rawValue: Int32(index)) {
@ -255,25 +261,27 @@ class WebSearchControllerNode: ASDisplayNode {
}
}))
let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil)
self.recentDisposable = (combineLatest(webSearchRecentQueries(postbox: self.context.account.postbox), self.webSearchInterfaceStatePromise.get())
|> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in
if let strongSelf = self {
var entries: [WebSearchRecentQueryEntry] = []
for i in 0 ..< queries.count {
entries.append(WebSearchRecentQueryEntry(index: i, query: queries[i]))
if !attachment {
let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil)
self.recentDisposable = (combineLatest(webSearchRecentQueries(postbox: self.context.account.postbox), self.webSearchInterfaceStatePromise.get())
|> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in
if let strongSelf = self {
var entries: [WebSearchRecentQueryEntry] = []
for i in 0 ..< queries.count {
entries.append(WebSearchRecentQueryEntry(index: i, query: queries[i]))
}
let header = ChatListSearchItemHeader(type: .recentPeers, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, actionTitle: interfaceState.presentationData.strings.WebSearch_RecentSectionClear, action: {
_ = clearRecentWebSearchQueries(postbox: strongSelf.context.account.postbox).start()
})
let previousEntries = previousRecentItems.swap(entries)
let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.context.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
let header = ChatListSearchItemHeader(type: .recentPeers, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, actionTitle: interfaceState.presentationData.strings.WebSearch_RecentSectionClear, action: {
_ = clearRecentWebSearchQueries(postbox: strongSelf.context.account.postbox).start()
})
let previousEntries = previousRecentItems.swap(entries)
let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.context.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
})
})
}
self.recentQueriesNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
@ -458,7 +466,7 @@ class WebSearchControllerNode: ASDisplayNode {
insets.top += segmentedHeight
insets.bottom += toolbarHeight
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(size: layout.size)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in })
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(size: layout.size, insets: insets)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in })
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
@ -530,7 +538,11 @@ class WebSearchControllerNode: ASDisplayNode {
self?.dismissInput?()
}
self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode)
if self.recentQueriesNode.supernode != nil {
self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode)
} else {
self.addSubnode(gridNode)
}
self.gridNode = gridNode
self.currentEntries = nil
let directionMultiplier: CGFloat

View file

@ -16,13 +16,15 @@ final class WebSearchNavigationContentNode: NavigationBarContentNode {
private var queryUpdated: ((String) -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings) {
var cancel: (() -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings, attachment: Bool) {
self.theme = theme
self.strings = strings
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern)
self.searchBar.hasCancelButton = false
self.searchBar.placeholderString = NSAttributedString(string: strings.Common_Search, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
self.searchBar.hasCancelButton = attachment
self.searchBar.placeholderString = NSAttributedString(string: attachment ? strings.Attachment_SearchWeb : strings.Common_Search, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
super.init()
@ -36,6 +38,9 @@ final class WebSearchNavigationContentNode: NavigationBarContentNode {
self?.queryUpdated?(query)
}
}
self.searchBar.cancel = { [weak self] in
self?.cancel?()
}
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {