mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Attachment menu improvements
This commit is contained in:
parent
2efbb9170f
commit
d811f5f160
BIN
Telegram/Telegram-iOS/Resources/Files.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Files.tgs
Normal file
Binary file not shown.
@ -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";
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
- (void)resumePreview;
|
||||
- (void)pausePreview;
|
||||
|
||||
- (void)removeCorners;
|
||||
|
||||
- (void)setZoomedProgress:(CGFloat)progress;
|
||||
|
||||
- (void)saveStartImage:(void (^)(void))completion;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
@ -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 {
|
||||
|
||||
@ -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)];
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -159,6 +159,7 @@
|
||||
backingItem.editingContext = self.editingContext;
|
||||
backingItem.stickersContext = self.stickersContext;
|
||||
backingItem.asFile = self.asFile;
|
||||
backingItem.immediateThumbnailImage = self.immediateThumbnailImage;
|
||||
_backingItem = backingItem;
|
||||
}
|
||||
return _backingItem;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ swift_library(
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
"//submodules/WallpaperResources:WallpaperResources",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
43
submodules/MediaPickerUI/BUILD
Normal file
43
submodules/MediaPickerUI/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
61
submodules/MediaPickerUI/Sources/FetchAssets.swift
Normal file
61
submodules/MediaPickerUI/Sources/FetchAssets.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
284
submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift
Normal file
284
submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift
Normal 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)
|
||||
}
|
||||
264
submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift
Normal file
264
submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
74
submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift
Normal file
74
submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1198
submodules/MediaPickerUI/Sources/MediaPickerScreen.swift
Normal file
1198
submodules/MediaPickerUI/Sources/MediaPickerScreen.swift
Normal file
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,9 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display:Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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! {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-3.pdf",
|
||||
"filename" : "Icon-5.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
||||
@ -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
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Icon-5.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Icon-5.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/OpenCamera.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/OpenCamera.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-7.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
180
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/OpenCamera.imageset/Icon-7.pdf
vendored
Normal file
180
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/OpenCamera.imageset/Icon-7.pdf
vendored
Normal 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
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-4.pdf",
|
||||
"filename" : "Type=Default-5.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
||||
@ -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
|
||||
113
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Type=Default-5.pdf
vendored
Normal file
113
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Type=Default-5.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/File.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/File.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-6.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
152
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/File.imageset/Icon-6.pdf
vendored
Normal file
152
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/File.imageset/Icon-6.pdf
vendored
Normal 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
|
||||
@ -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
|
||||
}
|
||||
|
||||
103
submodules/TelegramUI/Sources/AttachmentFileEmptyItem.swift
Normal file
103
submodules/TelegramUI/Sources/AttachmentFileEmptyItem.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
566
submodules/TelegramUI/Sources/AttachmentFileSearchItem.swift
Normal file
566
submodules/TelegramUI/Sources/AttachmentFileSearchItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -31,6 +31,7 @@ swift_library(
|
||||
"//submodules/SegmentedControlNode:SegmentedControlNode",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/AttachmentUI:AttachmentUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user