Attachment menu improvements

This commit is contained in:
Ilya Laktyushin 2022-02-21 10:11:53 +03:00
parent 2efbb9170f
commit d811f5f160
80 changed files with 6104 additions and 1273 deletions

Binary file not shown.

View File

@ -7267,9 +7267,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

@ -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

@ -44,10 +44,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
@ -286,7 +285,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
@ -344,24 +342,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

View File

@ -12,7 +12,6 @@ import TelegramStringFormatting
import UIKitRuntimeUtils
public enum AttachmentButtonType: Equatable {
case camera
case gallery
case file
case location
@ -25,18 +24,33 @@ public protocol AttachmentContainable: ViewController {
var requestAttachmentMenuExpansion: () -> Void { get set }
}
public enum AttachmentMediaPickerSendMode {
case media
case files
}
public protocol AttachmentMediaPickerContext {
var selectionCount: Signal<Int, NoError> { get }
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?
@ -53,7 +67,7 @@ public class AttachmentController: ViewController {
private let captionDisposable = MetaDisposable()
private let mediaSelectionCountDisposable = MetaDisposable()
private var mediaPickerContext: AttachmentMediaPickerContext? {
fileprivate var mediaPickerContext: AttachmentMediaPickerContext? {
didSet {
if let mediaPickerContext = self.mediaPickerContext {
self.captionDisposable.set((mediaPickerContext.caption
@ -82,10 +96,9 @@ 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()
@ -133,9 +146,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()
}
@ -272,6 +285,10 @@ public class AttachmentController: ViewController {
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)))
}
func scrollToTop() {
self.currentController?.scrollToTop?()
}
private var isCollapsed: Bool = false
private var isUpdatingContainer = false
private var switchingController = false
@ -279,31 +296,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 +310,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.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(transition: transition)
}
self.isUpdatingContainer = false
}
}
}
@ -324,15 +346,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) {

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
@ -83,40 +84,29 @@ private final class AttachButtonComponent: CombinedComponent {
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
}
@ -129,8 +119,6 @@ private final class AttachButtonComponent: CombinedComponent {
transition: context.transition
)
print(animationName ?? "")
let title = title.update(
component: Text(
text: name,
@ -169,6 +157,7 @@ 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?
@ -183,7 +172,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
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 +187,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)
@ -362,7 +351,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 +376,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.backgroundColor = presentationData.theme.actionSheet.itemBackgroundColor
strongSelf.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.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() {
@ -407,6 +416,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
effect = UIBlurEffect(style: .dark)
}
let effectView = UIVisualEffectView(effect: effect)
effectView.frame = self.containerNode.bounds
self.effectView = effectView
self.containerNode.view.insertSubview(effectView, at: 0)
}
@ -644,7 +654,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
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)
containerTransition.updateFrame(view: effectView, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: containerFrame.height + 44.0)))
}
let _ = self.updateScrollLayoutIfNeeded(force: isCollapsedUpdated || isSelectingUpdated, transition: containerTransition)

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()
}
}
@ -432,35 +446,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

@ -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))
}

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

@ -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

@ -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,16 @@ 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 +387,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,131 +430,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 {
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 {
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 {
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 {
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.displayFileInfo {
descriptionString = stringForDuration(Int32(duration))
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
}
} else {
if !item.isGlobalSearchResult {
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 ?? ""
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.displayFileInfo {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)"
}
} else {
if !item.isGlobalSearchResult {
descriptionString = "\(dateString)"
}
}
if item.isGlobalSearchResult {
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 {
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 {
descriptionString = stringForDuration(Int32(duration))
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
}
} else {
if !item.isGlobalSearchResult {
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 ?? ""
//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 {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)"
}
} else {
if !item.isGlobalSearchResult {
descriptionString = "\(dateString)"
}
if !item.isGlobalSearchResult {
descriptionString = "\(dateString)"
}
if item.isGlobalSearchResult {
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 {
@ -538,44 +596,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
//TODO:localize
let fileName: String = "Photo"
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 {
descriptionString = "\(dateString)"
}
if item.isGlobalSearchResult {
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
@ -590,11 +621,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
@ -614,7 +645,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)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
@ -655,7 +686,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) {
@ -695,7 +726,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()))
@ -706,7 +737,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 {
@ -729,24 +764,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()
}
}
@ -754,6 +791,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)
@ -764,7 +804,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
@ -789,8 +829,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
@ -842,10 +883,40 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
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 - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), 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))
@ -876,17 +947,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)
@ -954,6 +1019,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()
}
}
})
}
@ -1023,7 +1126,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 {
@ -1044,7 +1151,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, 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)
@ -1054,7 +1161,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
@ -1202,8 +1309,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 interaction = self.interaction, let message = item.message {
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,17 +48,19 @@ 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 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, 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, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
self.presentationData = presentationData
self.context = context
self.chatLocation = chatLocation
@ -65,7 +68,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
@ -73,21 +76,27 @@ public final class ListMessageItem: ListViewItem {
self.selection = selection
self.hintIsLink = hintIsLink
self.isGlobalSearchResult = isGlobalSearchResult
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
}
}
@ -97,7 +106,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)
@ -130,7 +139,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 {
@ -148,17 +157,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()
}
}
}
}
@ -179,6 +196,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

@ -0,0 +1,43 @@
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",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,61 @@
import Foundation
import UIKit
import Photos
import SwiftSignalKit
private let imageManager = PHCachingImageManager()
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 {
imageManager.cancelImageRequest(token)
}
}
}
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,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,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)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,886 @@
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 {
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.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 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)]
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 {
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

@ -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

@ -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/ReportCopyright")
case .personalDetails:
title = presentationData.strings.ReportPeer_ReasonPersonalDetails
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportCopyright")
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

@ -1835,6 +1835,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)
}
@ -1847,7 +1852,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

@ -137,6 +137,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1278304028] = { return Api.storage.FileType.parse_fileMp4($0) }
dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) }
dict[1338747336] = { return Api.messages.ArchivedStickers.parse_archivedStickers($0) }
dict[-2132064081] = { return Api.GroupCallStreamChannel.parse_groupCallStreamChannel($0) }
dict[406307684] = { return Api.InputEncryptedFile.parse_inputEncryptedFileEmpty($0) }
dict[1690108678] = { return Api.InputEncryptedFile.parse_inputEncryptedFileUploaded($0) }
dict[1511503333] = { return Api.InputEncryptedFile.parse_inputEncryptedFile($0) }
@ -146,6 +147,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-341428482] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) }
dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) }
dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) }
dict[-790330702] = { return Api.phone.GroupCallStreamChannels.parse_groupCallStreamChannels($0) }
dict[289586518] = { return Api.SavedContact.parse_savedPhoneContact($0) }
dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) }
dict[872119224] = { return Api.auth.Authorization.parse_authorization($0) }
@ -514,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) }
@ -1098,6 +1102,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.messages.ArchivedStickers:
_1.serialize(buffer, boxed)
case let _1 as Api.GroupCallStreamChannel:
_1.serialize(buffer, boxed)
case let _1 as Api.InputEncryptedFile:
_1.serialize(buffer, boxed)
case let _1 as Api.account.Takeout:
@ -1108,6 +1114,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.messages.SentEncryptedMessage:
_1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStreamChannels:
_1.serialize(buffer, boxed)
case let _1 as Api.SavedContact:
_1.serialize(buffer, boxed)
case let _1 as Api.ExportedMessageLink:

View File

@ -3568,6 +3568,48 @@ public extension Api {
}
}
}
public enum GroupCallStreamChannel: TypeConstructorDescription {
case groupCallStreamChannel(channel: Int32, scale: Int32, lastTimestampMs: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .groupCallStreamChannel(let channel, let scale, let lastTimestampMs):
if boxed {
buffer.appendInt32(-2132064081)
}
serializeInt32(channel, buffer: buffer, boxed: false)
serializeInt32(scale, buffer: buffer, boxed: false)
serializeInt64(lastTimestampMs, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .groupCallStreamChannel(let channel, let scale, let lastTimestampMs):
return ("groupCallStreamChannel", [("channel", channel), ("scale", scale), ("lastTimestampMs", lastTimestampMs)])
}
}
public static func parse_groupCallStreamChannel(_ reader: BufferReader) -> GroupCallStreamChannel? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: Int64?
_3 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.GroupCallStreamChannel.groupCallStreamChannel(channel: _1!, scale: _2!, lastTimestampMs: _3!)
}
else {
return nil
}
}
}
public enum InputEncryptedFile: TypeConstructorDescription {
case inputEncryptedFileEmpty
@ -12968,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 {
@ -13018,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
}
}
@ -13040,6 +13096,10 @@ public extension Api {
return ("inputReportReasonGeoIrrelevant", [])
case .inputReportReasonFake:
return ("inputReportReasonFake", [])
case .inputReportReasonIllegalDrugs:
return ("inputReportReasonIllegalDrugs", [])
case .inputReportReasonPersonalDetails:
return ("inputReportReasonPersonalDetails", [])
}
}
@ -13067,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

@ -1722,6 +1722,46 @@ public struct photos {
}
public extension Api {
public struct phone {
public enum GroupCallStreamChannels: TypeConstructorDescription {
case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .groupCallStreamChannels(let channels):
if boxed {
buffer.appendInt32(-790330702)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(channels.count))
for item in channels {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .groupCallStreamChannels(let channels):
return ("groupCallStreamChannels", [("channels", channels)])
}
}
public static func parse_groupCallStreamChannels(_ reader: BufferReader) -> GroupCallStreamChannels? {
var _1: [Api.GroupCallStreamChannel]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallStreamChannel.self)
}
let _c1 = _1 != nil
if _c1 {
return Api.phone.GroupCallStreamChannels.groupCallStreamChannels(channels: _1!)
}
else {
return nil
}
}
}
public enum JoinAsPeers: TypeConstructorDescription {
case joinAsPeers(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
@ -4657,6 +4697,22 @@ public extension Api {
return result
})
}
public static func searchSentMedia(q: String, filter: Api.MessagesFilter, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
let buffer = Buffer()
buffer.appendInt32(276705696)
serializeString(q, buffer: buffer, boxed: false)
filter.serialize(buffer, true)
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.searchSentMedia", parameters: [("q", q), ("filter", filter), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in
let reader = BufferReader(buffer)
var result: Api.messages.Messages?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.Messages
}
return result
})
}
}
public struct channels {
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
@ -6208,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>) {
@ -8603,6 +8673,20 @@ public extension Api {
return result
})
}
public static func getGroupCallStreamChannels(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.phone.GroupCallStreamChannels>) {
let buffer = Buffer()
buffer.appendInt32(447879488)
call.serialize(buffer, true)
return (FunctionDescription(name: "phone.getGroupCallStreamChannels", parameters: [("call", call)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.phone.GroupCallStreamChannels? in
let reader = BufferReader(buffer)
var result: Api.phone.GroupCallStreamChannels?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.phone.GroupCallStreamChannels
}
return result
})
}
}
}
}

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 138
return 139
}
public func parseMessage(_ data: Data!) -> Any! {

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

@ -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

@ -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,34 @@ 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 [:] })
return ListMessageItem(presentationData: ChatPresentationData(presentationData: arguments.context.sharedContext.currentPresentationData.with({$0})), 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
@ -151,30 +160,98 @@ private class AttachmentFileControllerImpl: ItemListController, AttachmentContai
public var requestAttachmentMenuExpansion: () -> Void = {}
}
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
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 {
@ -185,5 +262,11 @@ public func attachmentFileController(context: AccountContext, presentGallery: @e
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,7 +10321,8 @@ 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 {
@ -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 }
@ -10528,6 +10568,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 +10669,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 +10739,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 +10857,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 +10973,67 @@ 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)
}
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 +11071,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 +11169,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 +11183,7 @@ 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
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in
if let strongSelf = self {
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
@ -11110,7 +11213,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))
}
})
}

View File

@ -11,6 +11,7 @@ import AccountContext
import ContactListUI
import SearchUI
import AttachmentUI
import SearchBarNode
class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable {
private let context: AccountContext
@ -56,7 +57,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var searchContentNode: NavigationBarSearchContentNode?
private var searchContentNode: NavigationBarContentNode?
var displayNavigationActivity: Bool = false {
didSet {
@ -98,10 +99,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 +128,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 +141,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 +154,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,13 +202,13 @@ 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
@ -268,10 +274,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 +302,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)
}
}
@ -303,3 +326,57 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.deactivateSearch()
}
}
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)
}
}

View File

@ -50,6 +50,8 @@ 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
@ -143,6 +145,80 @@ final class ContactSelectionControllerNode: ASDisplayNode {
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

@ -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,26 @@ 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.controllerNode.dismissInput?()
strongSelf.controllerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
self?.dismissed()
self?.dismiss()
})
}
}
self.navigationBar?.setContentNode(navigationContentNode, animated: false)
if let query = searchQuery {
navigationContentNode.setQuery(query)
@ -274,7 +290,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)
}
}
@ -322,11 +338,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 +525,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 +541,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 - 3.0) / 4.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) {