diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c272a9d0ed..e60c350963 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -884,6 +884,11 @@ public enum CollectibleItemInfoScreenSubject { case username(String) } +public enum StorySearchControllerScope { + case query(String) + case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue, address: MediaArea.Address?) +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -970,7 +975,7 @@ public protocol SharedAccountContext: AnyObject { func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController - func makeStorySearchController(context: AccountContext, query: String) -> ViewController + func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController diff --git a/submodules/LocationUI/Sources/LocationAnnotation.swift b/submodules/LocationUI/Sources/LocationAnnotation.swift index eb0aa0d039..78f0375eb0 100644 --- a/submodules/LocationUI/Sources/LocationAnnotation.swift +++ b/submodules/LocationUI/Sources/LocationAnnotation.swift @@ -26,10 +26,10 @@ private func generateSmallBackgroundImage(color: UIColor) -> UIImage? { }) } -class LocationPinAnnotation: NSObject, MKAnnotation { +public class LocationPinAnnotation: NSObject, MKAnnotation { let context: AccountContext let theme: PresentationTheme - var coordinate: CLLocationCoordinate2D { + public var coordinate: CLLocationCoordinate2D { willSet { self.willChangeValue(forKey: "coordinate") } @@ -55,10 +55,10 @@ class LocationPinAnnotation: NSObject, MKAnnotation { var isSelf = false var selfPeer: EnginePeer? - var title: String? = "" - var subtitle: String? = "" + public var title: String? = "" + public var subtitle: String? = "" - init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?) { + public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?) { self.context = context self.theme = theme self.location = nil @@ -71,7 +71,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { super.init() } - init(context: AccountContext, theme: PresentationTheme, location: TelegramMediaMap, queryId: Int64?, resultId: String?, forcedSelection: Bool = false) { + public init(context: AccountContext, theme: PresentationTheme, location: TelegramMediaMap, queryId: Int64?, resultId: String?, forcedSelection: Bool = false) { self.context = context self.theme = theme self.location = location @@ -84,7 +84,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { super.init() } - init(context: AccountContext, theme: PresentationTheme, message: EngineMessage, selfPeer: EnginePeer?, isSelf: Bool, heading: Int32?) { + public init(context: AccountContext, theme: PresentationTheme, message: EngineMessage, selfPeer: EnginePeer?, isSelf: Bool, heading: Int32?) { self.context = context self.theme = theme self.location = nil @@ -104,7 +104,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { super.init() } - var id: String { + public var id: String { if let message = self.message { return "\(message.id.id)" } else if let peer = self.peer { @@ -157,7 +157,7 @@ private func removePulseAnimations(layer: CALayer) { layer.removeAnimation(forKey: "pulse-opacity") } -class LocationPinAnnotationView: MKAnnotationView { +public class LocationPinAnnotationView: MKAnnotationView { let shadowNode: ASImageNode let pulseNode: ASImageNode let backgroundNode: ASImageNode @@ -178,17 +178,17 @@ class LocationPinAnnotationView: MKAnnotationView { var headingKvoToken: NSKeyValueObservation? - override class var layerClass: AnyClass { + override public class var layerClass: AnyClass { return LocationPinAnnotationLayer.self } - func setZPosition(_ zPosition: CGFloat?) { + public func setZPosition(_ zPosition: CGFloat?) { if let layer = self.layer as? LocationPinAnnotationLayer { layer.customZPosition = zPosition } } - init(annotation: LocationPinAnnotation) { + public init(annotation: LocationPinAnnotation) { self.shadowNode = ASImageNode() self.shadowNode.image = UIImage(bundleImageName: "Location/PinShadow") if let image = self.shadowNode.image { @@ -244,7 +244,7 @@ class LocationPinAnnotationView: MKAnnotationView { self.annotation = annotation } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -252,7 +252,7 @@ class LocationPinAnnotationView: MKAnnotationView { self.headingKvoToken?.invalidate() } - var defaultZPosition: CGFloat { + public var defaultZPosition: CGFloat { if let annotation = self.annotation as? LocationPinAnnotation { if annotation.forcedSelection { return 0.0 @@ -266,7 +266,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override var annotation: MKAnnotation? { + override public var annotation: MKAnnotation? { didSet { if let annotation = self.annotation as? LocationPinAnnotation { if let message = annotation.message { @@ -363,14 +363,14 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override func prepareForReuse() { + override public func prepareForReuse() { self.previousPeerId = nil self.smallNode.isHidden = true self.backgroundNode.isHidden = false self.appeared = false } - override func setSelected(_ selected: Bool, animated: Bool) { + override public func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) if let annotation = self.annotation as? LocationPinAnnotation { @@ -547,7 +547,7 @@ class LocationPinAnnotationView: MKAnnotationView { } var previousPeerId: EnginePeer.Id? - func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { let avatarNode: AvatarNode if let currentAvatarNode = self.avatarNode { avatarNode = currentAvatarNode @@ -566,7 +566,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if let labelNode = self.labelNode { @@ -589,7 +589,7 @@ class LocationPinAnnotationView: MKAnnotationView { } var isRaised = false - func setRaised(_ raised: Bool, animated: Bool, completion: @escaping () -> Void = {}) { + public func setRaised(_ raised: Bool, animated: Bool, completion: @escaping () -> Void = {}) { guard raised != self.isRaised else { return } @@ -625,7 +625,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - func setCustom(_ custom: Bool, animated: Bool) { + public func setCustom(_ custom: Bool, animated: Bool) { if let annotation = self.annotation as? LocationPinAnnotation { self.iconNode.setSignal(venueIcon(engine: annotation.context.engine, type: "", background: false)) } @@ -676,7 +676,7 @@ class LocationPinAnnotationView: MKAnnotationView { self.dotNode.isHidden = !custom } - func animateAppearance() { + public func animateAppearance() { guard let annotation = self.annotation as? LocationPinAnnotation, annotation.location != nil && !annotation.forcedSelection else { return } @@ -694,7 +694,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override func layoutSubviews() { + override public func layoutSubviews() { super.layoutSubviews() guard !self.animating else { diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index 1ce0bb28da..4c3bca6ec8 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -11,7 +11,7 @@ import AppBundle import SolidRoundedButtonNode import ShimmerEffect -final class LocationInfoListItem: ListViewItem { +public final class LocationInfoListItem: ListViewItem { let presentationData: ItemListPresentationData let engine: TelegramEngine let location: TelegramMediaMap @@ -75,7 +75,7 @@ final class LocationInfoListItem: ListViewItem { } } -final class LocationInfoListItemNode: ListViewItemNode { +public final class LocationInfoListItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? @@ -91,7 +91,7 @@ final class LocationInfoListItemNode: ListViewItemNode { private var layoutParams: ListViewItemLayoutParams? private var absoluteLocation: (CGRect, CGSize)? - required init() { + required public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.buttonNode = HighlightableButtonNode() @@ -127,7 +127,7 @@ final class LocationInfoListItemNode: ListViewItemNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = self.item { let makeLayout = self.asyncLayout() let (nodeLayout, nodeApply) = makeLayout(item, params) @@ -137,7 +137,7 @@ final class LocationInfoListItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + public func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { let currentItem = self.item let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -205,6 +205,8 @@ final class LocationInfoListItemNode: ListViewItemNode { strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor } + strongSelf.backgroundNode.isHidden = params.isStandalone + let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) if let updatedLocation = updatedLocation { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true)) @@ -384,11 +386,11 @@ final class LocationInfoListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index 44472fe5c2..757b7b8292 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -34,7 +34,7 @@ private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) -> })?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0) } -final class LocationMapHeaderNode: ASDisplayNode { +public final class LocationMapHeaderNode: ASDisplayNode { private var presentationData: PresentationData private let toggleMapModeSelection: () -> Void private let goToUserLocation: () -> Void @@ -44,8 +44,8 @@ final class LocationMapHeaderNode: ASDisplayNode { private var displayingPlacesButton = false private var proximityNotification: Bool? - let mapNode: LocationMapNode - var trackingMode: LocationTrackingMode = .none + public let mapNode: LocationMapNode + public var trackingMode: LocationTrackingMode = .none private let optionsBackgroundNode: ASImageNode private let optionsSeparatorNode: ASDisplayNode @@ -59,7 +59,7 @@ final class LocationMapHeaderNode: ASDisplayNode { private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGSize)? - init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, showPlacesInThisArea: @escaping () -> Void = {}) { + public init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, showPlacesInThisArea: @escaping () -> Void = {}) { self.presentationData = presentationData self.toggleMapModeSelection = toggleMapModeSelection self.goToUserLocation = goToUserLocation @@ -131,7 +131,7 @@ final class LocationMapHeaderNode: ASDisplayNode { self.placesButtonNode.addTarget(self, action: #selector(self.placesPressed), forControlEvents: .touchUpInside) } - func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) { + public func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) { self.mapNode.mapMode = mapMode self.trackingMode = trackingMode self.infoButtonNode.isSelected = displayingMapModeOptions @@ -149,7 +149,7 @@ final class LocationMapHeaderNode: ASDisplayNode { } } - func updatePresentationData(_ presentationData: PresentationData) { + public func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) @@ -177,13 +177,13 @@ final class LocationMapHeaderNode: ASDisplayNode { } } - func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { + public func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight, topPadding, offset, size) let mapHeight: CGFloat = floor(layout.size.height * 1.3) let mapFrame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - mapHeight + navigationBarHeight) / 2.0) + offset, width: size.width, height: mapHeight) transition.updateFrame(node: self.mapNode, frame: mapFrame) - self.mapNode.updateLayout(size: mapFrame.size) + self.mapNode.updateLayout(size: mapFrame.size, topPadding: layout.intrinsicInsets.top) let inset: CGFloat = 6.0 @@ -191,6 +191,8 @@ final class LocationMapHeaderNode: ASDisplayNode { let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: self.displayingPlacesButton ? navigationBarHeight + topPadding + inset : 0.0), size: placesButtonSize) transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame) transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize)) + transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) + transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0)) @@ -214,7 +216,7 @@ final class LocationMapHeaderNode: ASDisplayNode { alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha) } - var forceIsHidden: Bool = false { + public var forceIsHidden: Bool = false { didSet { if let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout { self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: .immediate) @@ -222,11 +224,11 @@ final class LocationMapHeaderNode: ASDisplayNode { } } - func updateHighlight(_ highlighted: Bool) { + public func updateHighlight(_ highlighted: Bool) { self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted) } - func proximityButtonFrame() -> CGRect? { + public func proximityButtonFrame() -> CGRect? { if self.notificationButtonNode.alpha > 0.0 { return self.optionsBackgroundNode.view.convert(self.notificationButtonNode.frame, to: self.view) } else { diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index def8ec6830..1f9601b028 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -5,8 +5,6 @@ import Display import SwiftSignalKit import MapKit -let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) -let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) private let pinOffset = CGPoint(x: 0.0, y: 33.0) public enum LocationMapMode { @@ -128,7 +126,10 @@ private func generateProximityDim(size: CGSize) -> UIImage { })! } -final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { +public final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { + public static let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) + public static let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) + class ProximityCircleRenderer: MKCircleRenderer { override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { super.draw(mapRect, zoomScale: zoomScale, in: context) @@ -204,7 +205,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } private var circleOverlay: MKCircle? - var activeProximityRadius: Double? { + public var activeProximityRadius: Double? { didSet { if let activeProximityRadius = self.activeProximityRadius { if let circleOverlay = self.circleOverlay { @@ -225,7 +226,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - override init() { + override public init() { self.pickerAnnotationContainerView = PickerAnnotationContainerView() self.pickerAnnotationContainerView.isHidden = true @@ -236,7 +237,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { }) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.headingArrowView = UIImageView() @@ -292,7 +293,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var trackingMode: LocationTrackingMode = .none { + public var trackingMode: LocationTrackingMode = .none { didSet { self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode if self.trackingMode == .followWithHeading && self.headingArrowView?.image != nil { @@ -303,11 +304,18 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } + var topPadding: CGFloat = 0.0 var mapOffset: CGFloat = 0.0 - func setMapCenter(coordinate: CLLocationCoordinate2D, radius: Double, insets: UIEdgeInsets, offset: CGFloat, animated: Bool = false) { + var hasValidLayout: Bool = false + var pendingSetMapCenter: (coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan, offset: CGPoint, isUserLocation: Bool, hidePicker: Bool, animated: Bool)? + + public func setMapCenter(coordinate: CLLocationCoordinate2D, radius: Double, insets: UIEdgeInsets, offset: CGFloat, animated: Bool = false) { self.mapOffset = offset self.ignoreRegionChanges = true + var insets = insets + insets.top += self.topPadding + let mapRect = MKMapRect(region: MKCoordinateRegion(center: coordinate, latitudinalMeters: radius * 2.0, longitudinalMeters: radius * 2.0)) self.mapView?.setVisibleMapRect(mapRect, edgePadding: insets, animated: animated) self.ignoreRegionChanges = false @@ -315,16 +323,34 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.proximityDimView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY + offset) } - func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) { + public func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) { + self.pendingSetMapCenter = ( + coordinate, span, offset, isUserLocation, hidePicker, animated + ) + + if self.hasValidLayout { + self.applyPendingSetMapCenter() + } + } + + private func applyPendingSetMapCenter() { + if !self.hasValidLayout { + return + } + guard let (coordinate, span, offset, isUserLocation, hidePicker, animated) = self.pendingSetMapCenter else { + return + } + self.pendingSetMapCenter = nil + let region = MKCoordinateRegion(center: coordinate, span: span) self.ignoreRegionChanges = true - if offset == CGPoint() { + if offset == CGPoint() && self.topPadding == 0.0 { self.mapView?.setRegion(region, animated: animated) } else { let mapRect = MKMapRect(region: region) - self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y, left: offset.x, bottom: 0.0, right: 0.0), animated: animated) + self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y + self.topPadding, left: offset.x, bottom: 0.0, right: 0.0), animated: animated) } - self.ignoreRegionChanges = false + self.ignoreRegionChanges = false if isUserLocation { if !self.returnedToUserLocation { @@ -339,7 +365,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { guard !self.ignoreRegionChanges, let scrollView = mapView.subviews.first, let gestureRecognizers = scrollView.gestureRecognizers else { return } @@ -356,7 +382,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { let wasDragging = self.isDragging if self.isDragging { self.isDragging = false @@ -372,7 +398,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + public func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { guard let location = userLocation.location else { return } @@ -380,11 +406,11 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.locationPromise.set(.single(location)) } - func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { + public func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { self.locationPromise.set(.single(nil)) } - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { if annotation === mapView.userLocation { return nil } @@ -400,7 +426,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { return nil } - func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { + public func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { for view in views { if view.annotation is MKUserLocation { self.defaultUserLocationAnnotation = view.annotation @@ -424,7 +450,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { guard let annotation = view.annotation as? LocationPinAnnotation else { return } @@ -440,7 +466,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + public func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { if let view = view as? LocationPinAnnotationView { Queue.mainQueue().after(0.2) { view.setZPosition(view.defaultZPosition) @@ -459,7 +485,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let circle = overlay as? MKCircle { let renderer = ProximityCircleRenderer(circle: circle) renderer.fillColor = .clear @@ -472,7 +498,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var distancesToAllAnnotations: Signal<[Double], NoError> { + public var distancesToAllAnnotations: Signal<[Double], NoError> { let poll = Signal<[LocationPinAnnotation], NoError> { [weak self] subscriber in if let strongSelf = self { subscriber.putNext(strongSelf.annotations) @@ -497,30 +523,30 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var currentUserLocation: CLLocation? { + public var currentUserLocation: CLLocation? { return self.mapView?.userLocation.location } - var userLocation: Signal { + public var userLocation: Signal { return .single(self.currentUserLocation) |> then (self.locationPromise.get()) } - var mapCenterCoordinate: CLLocationCoordinate2D? { + public var mapCenterCoordinate: CLLocationCoordinate2D? { guard let mapView = self.mapView else { return nil } return mapView.convert(CGPoint(x: (mapView.frame.width + pinOffset.x) / 2.0, y: (mapView.frame.height + pinOffset.y) / 2.0), toCoordinateFrom: mapView) } - var mapSpan: MKCoordinateSpan? { + public var mapSpan: MKCoordinateSpan? { guard let mapView = self.mapView else { return nil } return mapView.region.span } - func resetAnnotationSelection() { + public func resetAnnotationSelection() { guard let mapView = self.mapView else { return } @@ -529,8 +555,8 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var pickerAnnotationView: LocationPinAnnotationView? = nil - var hasPickerAnnotation: Bool = false { + public var pickerAnnotationView: LocationPinAnnotationView? = nil + public var hasPickerAnnotation: Bool = false { didSet { if self.hasPickerAnnotation, let annotation = self.userLocationAnnotation { let pickerAnnotationView = LocationPinAnnotationView(annotation: annotation) @@ -544,7 +570,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func switchToPicking(raise: Bool = false, animated: Bool) { + public func switchToPicking(raise: Bool = false, animated: Bool) { guard self.hasPickerAnnotation else { return } @@ -561,8 +587,8 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.resetScheduledPin() } - var customUserLocationAnnotationView: LocationPinAnnotationView? = nil - var userLocationAnnotation: LocationPinAnnotation? = nil { + public var customUserLocationAnnotationView: LocationPinAnnotationView? = nil + public var userLocationAnnotation: LocationPinAnnotation? = nil { didSet { if let annotation = self.userLocationAnnotation { self.customUserLocationAnnotationView?.removeFromSuperview() @@ -582,7 +608,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var userHeading: CGFloat? = nil { + public var userHeading: CGFloat? = nil { didSet { if let heading = self.userHeading { self.headingArrowView?.isHidden = false @@ -594,7 +620,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var annotations: [LocationPinAnnotation] = [] { + public var annotations: [LocationPinAnnotation] = [] { didSet { guard let mapView = self.mapView else { return @@ -709,7 +735,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.pinDisposable.set(nil) } - func showAll(animated: Bool = true) { + public func showAll(animated: Bool = true) { guard let mapView = self.mapView else { return } @@ -736,11 +762,17 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func updateLayout(size: CGSize) { - self.proximityDimView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.mapOffset), size: size) + public func updateLayout(size: CGSize, topPadding: CGFloat) { + self.hasValidLayout = true + + self.topPadding = topPadding + + self.proximityDimView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.topPadding + self.mapOffset), size: size) self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width) if let pickerAnnotationView = self.pickerAnnotationView { pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0) } + + self.applyPendingSetMapCenter() } } diff --git a/submodules/LocationUI/Sources/LocationOptionsNode.swift b/submodules/LocationUI/Sources/LocationOptionsNode.swift index 50fe8aae00..3a7debafe2 100644 --- a/submodules/LocationUI/Sources/LocationOptionsNode.swift +++ b/submodules/LocationUI/Sources/LocationOptionsNode.swift @@ -6,14 +6,14 @@ import TelegramCore import TelegramPresentationData import SegmentedControlNode -final class LocationOptionsNode: ASDisplayNode { +public final class LocationOptionsNode: ASDisplayNode { private var presentationData: PresentationData private let backgroundNode: NavigationBackgroundNode private let separatorNode: ASDisplayNode private let segmentedControlNode: SegmentedControlNode - init(presentationData: PresentationData, updateMapMode: @escaping (LocationMapMode) -> Void) { + public init(presentationData: PresentationData, hasBackground: Bool = true, updateMapMode: @escaping (LocationMapMode) -> Void) { self.presentationData = presentationData self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) @@ -24,8 +24,11 @@ final class LocationOptionsNode: ASDisplayNode { super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) + if hasBackground { + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + } + self.addSubnode(self.segmentedControlNode) self.segmentedControlNode.selectedIndexChanged = { index in @@ -42,14 +45,14 @@ final class LocationOptionsNode: ASDisplayNode { } } - func updatePresentationData(_ presentationData: PresentationData) { + public func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundNode.update(size: size, transition: transition) diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index 3abf13348b..919912deab 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -101,7 +101,7 @@ func stringForEstimatedDuration(strings: PresentationStrings, time: Double, form } } -func throttledUserLocation(_ userLocation: Signal) -> Signal { +public func throttledUserLocation(_ userLocation: Signal) -> Signal { return userLocation |> reduceLeft(value: nil) { current, updated, emit -> CLLocation? in if let current = current { @@ -126,13 +126,13 @@ func throttledUserLocation(_ userLocation: Signal) -> Sign } } -enum ExpectedTravelTime: Equatable { +public enum ExpectedTravelTime: Equatable { case unknown case calculating case ready(Double) } -func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal { +public func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal { return Signal { subscriber in subscriber.putNext(.calculating) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 752e0a5f37..fb26971572 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -41,159 +41,159 @@ private struct LocationViewTransaction { let animated: Bool } -private enum LocationViewEntryId: Hashable { +public enum LocationViewEntryId: Hashable { case info case toggleLiveLocation(Bool) case liveLocation(UInt32) } -private enum LocationViewEntry: Comparable, Identifiable { +public enum LocationViewEntry: Comparable, Identifiable { case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Bool) case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?, Bool, EngineMessage.Id?) case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Int) - var stableId: LocationViewEntryId { + public var stableId: LocationViewEntryId { switch self { - case .info: - return .info - case let .toggleLiveLocation(_, _, _, _, _, additional, _): - return .toggleLiveLocation(additional) - case let .liveLocation(_, _, _, message, _, _, _, _, _): - return .liveLocation(message.stableId) + case .info: + return .info + case let .toggleLiveLocation(_, _, _, _, _, additional, _): + return .toggleLiveLocation(additional) + case let .liveLocation(_, _, _, message, _, _, _, _, _): + return .liveLocation(message.stableId) } } - static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { + public static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { switch lhs { - case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsHasEta): - if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { - return true - } else { - return false - } - case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout, lhsAdditional, lhsMessageId): - if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout, rhsAdditional, rhsMessageId) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout, lhsAdditional == rhsAdditional, lhsMessageId == rhsMessageId { - return true - } else { - return false - } - case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): - if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { - return true - } else { - return false - } - } - } - - static func <(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { - switch lhs { - case .info: - switch rhs { - case .info: - return false - case .toggleLiveLocation, .liveLocation: - return true - } - case let .toggleLiveLocation(_, _, _, _, _, lhsAdditional, _): - switch rhs { - case .info: - return false - case let .toggleLiveLocation(_, _, _, _, _, rhsAdditional, _): - return !lhsAdditional && rhsAdditional - case .liveLocation: - return true + case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsHasEta): + if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { + return true + } else { + return false + } + case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout, lhsAdditional, lhsMessageId): + if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout, rhsAdditional, rhsMessageId) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout, lhsAdditional == rhsAdditional, lhsMessageId == rhsMessageId { + return true + } else { + return false + } + case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): + if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { + return true + } else { + return false + } + } + } + + public static func <(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { + switch lhs { + case .info: + switch rhs { + case .info: + return false + case .toggleLiveLocation, .liveLocation: + return true + } + case let .toggleLiveLocation(_, _, _, _, _, lhsAdditional, _): + switch rhs { + case .info: + return false + case let .toggleLiveLocation(_, _, _, _, _, rhsAdditional, _): + return !lhsAdditional && rhsAdditional + case .liveLocation: + return true + } + case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): + switch rhs { + case .info, .toggleLiveLocation: + return false + case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): + return lhsIndex < rhsIndex } - case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): - switch rhs { - case .info, .toggleLiveLocation: - return false - case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): - return lhsIndex < rhsIndex - } } } func item(context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem { switch self { - case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime, hasEta): - let addressString: String? - if let address = address { - addressString = address - } else { - addressString = presentationData.strings.Map_Locating - } - let distanceString: String? - if let distance = distance { - distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).string - } else { - distanceString = nil - } - return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, hasEta: hasEta, action: { - interaction?.goToCoordinate(location.coordinate) - }, drivingAction: { - interaction?.requestDirections(location, nil, .driving) - }, transitAction: { - interaction?.requestDirections(location, nil, .transit) - }, walkingAction: { - interaction?.requestDirections(location, nil, .walking) - }) - case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout, additional, messageId): - var beginTimeAndTimeout: (Double, Double)? - if let beginTimstamp = beginTimstamp, let timeout = timeout { - beginTimeAndTimeout = (beginTimstamp, timeout) - } else { - beginTimeAndTimeout = nil - } + case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime, hasEta): + let addressString: String? + if let address = address { + addressString = address + } else { + addressString = presentationData.strings.Map_Locating + } + let distanceString: String? + if let distance = distance { + distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).string + } else { + distanceString = nil + } + return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, hasEta: hasEta, action: { + interaction?.goToCoordinate(location.coordinate) + }, drivingAction: { + interaction?.requestDirections(location, nil, .driving) + }, transitAction: { + interaction?.requestDirections(location, nil, .transit) + }, walkingAction: { + interaction?.requestDirections(location, nil, .walking) + }) + case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout, additional, messageId): + var beginTimeAndTimeout: (Double, Double)? + if let beginTimstamp = beginTimstamp, let timeout = timeout { + beginTimeAndTimeout = (beginTimstamp, timeout) + } else { + beginTimeAndTimeout = nil + } - let icon: LocationActionListItemIcon - if let timeout, Int32(timeout) != liveLocationIndefinitePeriod, !additional { - icon = .extendLiveLocation - } else if beginTimeAndTimeout != nil { - icon = .stopLiveLocation - } else { - icon = .liveLocation - } + let icon: LocationActionListItemIcon + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod, !additional { + icon = .extendLiveLocation + } else if beginTimeAndTimeout != nil { + icon = .stopLiveLocation + } else { + icon = .liveLocation + } - return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { - if beginTimeAndTimeout != nil { - if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { - if additional { - interaction?.stopLiveLocation() - } else { - interaction?.sendLiveLocation(nil, true, messageId) - } - } else { + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { + if beginTimeAndTimeout != nil { + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { + if additional { interaction?.stopLiveLocation() + } else { + interaction?.sendLiveLocation(nil, true, messageId) } } else { - interaction?.sendLiveLocation(nil, false, nil) + interaction?.stopLiveLocation() } - }, highlighted: { highlight in - interaction?.updateSendActionHighlight(highlight) - }) - case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): - var title: String? - if let author = message.author { - title = author.displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) + } else { + interaction?.sendLiveLocation(nil, false, nil) } - return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { - if let location = getLocation(from: message) { - interaction?.goToCoordinate(location.coordinate) - } - }, longTapAction: {}, drivingAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .driving) - } - }, transitAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .transit) - } - }, walkingAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .walking) - } - }) + }, highlighted: { highlight in + interaction?.updateSendActionHighlight(highlight) + }) + case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): + var title: String? + if let author = message.author { + title = author.displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) + } + return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { + if let location = getLocation(from: message) { + interaction?.goToCoordinate(location.coordinate) + } + }, longTapAction: {}, drivingAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .driving) + } + }, transitAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .transit) + } + }, walkingAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .walking) + } + }) } } } @@ -208,22 +208,22 @@ private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntr return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count, animated: animated) } -enum LocationViewLocation: Equatable { +public enum LocationViewLocation: Equatable { case initial case user case coordinate(CLLocationCoordinate2D, Bool) case custom } -struct LocationViewState { - var mapMode: LocationMapMode - var displayingMapModeOptions: Bool - var selectedLocation: LocationViewLocation - var trackingMode: LocationTrackingMode - var updatingProximityRadius: Int32? - var cancellingProximityRadius: Bool +public struct LocationViewState { + public var mapMode: LocationMapMode + public var displayingMapModeOptions: Bool + public var selectedLocation: LocationViewLocation + public var trackingMode: LocationTrackingMode + public var updatingProximityRadius: Int32? + public var cancellingProximityRadius: Bool - init() { + public init() { self.mapMode = .map self.displayingMapModeOptions = false self.selectedLocation = .initial @@ -614,12 +614,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan switch state.selectedLocation { case .initial: if previousState?.selectedLocation != .initial { - strongSelf.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: viewMapSpan, animated: previousState != nil) + strongSelf.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: LocationMapNode.viewMapSpan, animated: previousState != nil) } case let .coordinate(coordinate, defaultSpan): if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, previousCoordinate == coordinate { } else { - strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: defaultSpan ? defaultMapSpan : viewMapSpan, animated: true) + strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: defaultSpan ? LocationMapNode.defaultMapSpan : LocationMapNode.viewMapSpan, animated: true) } case .user: if previousState?.selectedLocation != .user, let userLocation = userLocation { diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index ca308f94ff..8e0a317db8 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -42,6 +42,7 @@ public protocol SparseItemGridBinding: AnyObject { func onTagTap() func didScroll() func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) + func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) func onBeginFastScrolling() func getShimmerColors() -> SparseItemGrid.ShimmerColors } @@ -1442,6 +1443,16 @@ public final class SparseItemGrid: ASDisplayNode { return 0.0 } } + + public var scrollingOffset: CGFloat { + if let currentViewportTransition = self.currentViewportTransition { + return currentViewportTransition.offset + } else if let currentViewport = self.currentViewport { + return currentViewport.offset + } else { + return 0.0 + } + } public var cancelExternalContentGestures: (() -> Void)? public var zoomLevelUpdated: ((ZoomLevel) -> Void)? @@ -1886,20 +1897,32 @@ public final class SparseItemGrid: ASDisplayNode { } private func offsetUpdated(viewport: Viewport, transition: ContainedViewLayoutTransition) { + guard let items = self.items else { + return + } + if self.currentViewportTransition != nil { return } + items.itemBinding.scrollingOffsetUpdated(transition: transition) + if let headerTextView = self.headerText?.view { headerTextView.layer.transform = CATransform3DMakeTranslation(0.0, -viewport.offset, 0.0) } } private func transitionOffsetUpdated(transition: ContainedViewLayoutTransition) { + guard let items = self.items else { + return + } + guard let currentViewportTransition = self.currentViewportTransition else { return } + items.itemBinding.scrollingOffsetUpdated(transition: transition) + if let headerTextView = self.headerText?.view { headerTextView.layer.transform = CATransform3DMakeTranslation(0.0, -currentViewportTransition.offset, 0.0) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 625ad769da..4713c79b43 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -290,6 +290,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1107729093] = { return Api.Game.parse_game($0) } dict[-1297942941] = { return Api.GeoPoint.parse_geoPoint($0) } dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) } + dict[-565420653] = { return Api.GeoPointAddress.parse_geoPointAddress($0) } dict[1934380235] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) } dict[-711498484] = { return Api.GroupCall.parse_groupCall($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } @@ -513,7 +514,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[577893055] = { return Api.MediaArea.parse_inputMediaAreaChannelPost($0) } dict[-1300094593] = { return Api.MediaArea.parse_inputMediaAreaVenue($0) } dict[1996756655] = { return Api.MediaArea.parse_mediaAreaChannelPost($0) } - dict[-544523486] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } + dict[-891992787] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } @@ -1627,6 +1628,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.GeoPoint: _1.serialize(buffer, boxed) + case let _1 as Api.GeoPointAddress: + _1.serialize(buffer, boxed) case let _1 as Api.GlobalPrivacySettings: _1.serialize(buffer, boxed) case let _1 as Api.GroupCall: diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 857e267a49..98bc7837e6 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -51,7 +51,7 @@ public extension Api { case inputMediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channel: Api.InputChannel, msgId: Int32) case inputMediaAreaVenue(coordinates: Api.MediaAreaCoordinates, queryId: Int64, resultId: String) case mediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channelId: Int64, msgId: Int32) - case mediaAreaGeoPoint(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint) + case mediaAreaGeoPoint(flags: Int32, coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, address: Api.GeoPointAddress?) case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) @@ -82,12 +82,14 @@ public extension Api { serializeInt64(channelId, buffer: buffer, boxed: false) serializeInt32(msgId, buffer: buffer, boxed: false) break - case .mediaAreaGeoPoint(let coordinates, let geo): + case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): if boxed { - buffer.appendInt32(-544523486) + buffer.appendInt32(-891992787) } + serializeInt32(flags, buffer: buffer, boxed: false) coordinates.serialize(buffer, true) geo.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {address!.serialize(buffer, true)} break case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): if boxed { @@ -127,8 +129,8 @@ public extension Api { return ("inputMediaAreaVenue", [("coordinates", coordinates as Any), ("queryId", queryId as Any), ("resultId", resultId as Any)]) case .mediaAreaChannelPost(let coordinates, let channelId, let msgId): return ("mediaAreaChannelPost", [("coordinates", coordinates as Any), ("channelId", channelId as Any), ("msgId", msgId as Any)]) - case .mediaAreaGeoPoint(let coordinates, let geo): - return ("mediaAreaGeoPoint", [("coordinates", coordinates as Any), ("geo", geo as Any)]) + case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): + return ("mediaAreaGeoPoint", [("flags", flags as Any), ("coordinates", coordinates as Any), ("geo", geo as Any), ("address", address as Any)]) case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): return ("mediaAreaSuggestedReaction", [("flags", flags as Any), ("coordinates", coordinates as Any), ("reaction", reaction as Any)]) case .mediaAreaUrl(let coordinates, let url): @@ -198,18 +200,26 @@ public extension Api { } } public static func parse_mediaAreaGeoPoint(_ reader: BufferReader) -> MediaArea? { - var _1: Api.MediaAreaCoordinates? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + _2 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates } - var _2: Api.GeoPoint? + var _3: Api.GeoPoint? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint + _3 = Api.parse(reader, signature: signature) as? Api.GeoPoint } + var _4: Api.GeoPointAddress? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.GeoPointAddress + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MediaArea.mediaAreaGeoPoint(coordinates: _1!, geo: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MediaArea.mediaAreaGeoPoint(flags: _1!, coordinates: _2!, geo: _3!, address: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 85128fcdfd..4b516b88f5 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -10394,16 +10394,15 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func searchPosts(flags: Int32, hashtag: String?, venueProvider: String?, venueId: String?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1391183841) + buffer.appendInt32(1827279210) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(venueProvider!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(venueId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {area!.serialize(buffer, true)} serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("venueProvider", String(describing: venueProvider)), ("venueId", String(describing: venueId)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in + return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in let reader = BufferReader(buffer) var result: Api.stories.FoundStories? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index dacc9ebe84..ac4e62f39d 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -648,6 +648,58 @@ public extension Api { } } +public extension Api { + enum GeoPointAddress: TypeConstructorDescription { + case geoPointAddress(flags: Int32, countryIso2: String, state: String?, city: String?, street: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .geoPointAddress(let flags, let countryIso2, let state, let city, let street): + if boxed { + buffer.appendInt32(-565420653) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(countryIso2, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(state!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(city!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(street!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .geoPointAddress(let flags, let countryIso2, let state, let city, let street): + return ("geoPointAddress", [("flags", flags as Any), ("countryIso2", countryIso2 as Any), ("state", state as Any), ("city", city as Any), ("street", street as Any)]) + } + } + + public static func parse_geoPointAddress(_ reader: BufferReader) -> GeoPointAddress? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.GeoPointAddress.geoPointAddress(flags: _1!, countryIso2: _2!, state: _3, city: _4, street: _5) + } + else { + return nil + } + } + + } +} public extension Api { enum GlobalPrivacySettings: TypeConstructorDescription { case globalPrivacySettings(flags: Int32) @@ -1262,53 +1314,3 @@ public extension Api { } } -public extension Api { - enum InputAppEvent: TypeConstructorDescription { - case inputAppEvent(time: Double, type: String, peer: Int64, data: Api.JSONValue) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputAppEvent(let time, let type, let peer, let data): - if boxed { - buffer.appendInt32(488313413) - } - serializeDouble(time, buffer: buffer, boxed: false) - serializeString(type, buffer: buffer, boxed: false) - serializeInt64(peer, buffer: buffer, boxed: false) - data.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputAppEvent(let time, let type, let peer, let data): - return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) - } - } - - public static func parse_inputAppEvent(_ reader: BufferReader) -> InputAppEvent? { - var _1: Double? - _1 = reader.readDouble() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: Api.JSONValue? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.JSONValue - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputAppEvent.inputAppEvent(time: _1!, type: _2!, peer: _3!, data: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index e936ab4fbc..711c158ee0 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -1,3 +1,53 @@ +public extension Api { + enum InputAppEvent: TypeConstructorDescription { + case inputAppEvent(time: Double, type: String, peer: Int64, data: Api.JSONValue) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputAppEvent(let time, let type, let peer, let data): + if boxed { + buffer.appendInt32(488313413) + } + serializeDouble(time, buffer: buffer, boxed: false) + serializeString(type, buffer: buffer, boxed: false) + serializeInt64(peer, buffer: buffer, boxed: false) + data.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputAppEvent(let time, let type, let peer, let data): + return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) + } + } + + public static func parse_inputAppEvent(_ reader: BufferReader) -> InputAppEvent? { + var _1: Double? + _1 = reader.readDouble() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Api.JSONValue? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.JSONValue + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputAppEvent.inputAppEvent(time: _1!, type: _2!, peer: _3!, data: _4!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputBotApp: TypeConstructorDescription { case inputBotAppID(id: Int64, accessHash: Int64) @@ -1262,43 +1312,3 @@ public extension Api { } } -public extension Api { - enum InputClientProxy: TypeConstructorDescription { - case inputClientProxy(address: String, port: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputClientProxy(let address, let port): - if boxed { - buffer.appendInt32(1968737087) - } - serializeString(address, buffer: buffer, boxed: false) - serializeInt32(port, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputClientProxy(let address, let port): - return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) - } - } - - public static func parse_inputClientProxy(_ reader: BufferReader) -> InputClientProxy? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputClientProxy.inputClientProxy(address: _1!, port: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 4634979e31..32c3e40b12 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1,3 +1,43 @@ +public extension Api { + enum InputClientProxy: TypeConstructorDescription { + case inputClientProxy(address: String, port: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputClientProxy(let address, let port): + if boxed { + buffer.appendInt32(1968737087) + } + serializeString(address, buffer: buffer, boxed: false) + serializeInt32(port, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputClientProxy(let address, let port): + return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) + } + } + + public static func parse_inputClientProxy(_ reader: BufferReader) -> InputClientProxy? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputClientProxy.inputClientProxy(address: _1!, port: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputCollectible: TypeConstructorDescription { case inputCollectiblePhone(phone: String) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index dc3d54acc7..4612d48023 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -482,7 +482,7 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return nil case .inputMediaAreaVenue: return nil - case let .mediaAreaGeoPoint(coordinates, geo): + case let .mediaAreaGeoPoint(_, coordinates, geo, address): let latitude: Double let longitude: Double switch geo { @@ -493,7 +493,21 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { latitude = 0.0 longitude = 0.0 } - return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: nil, queryId: nil, resultId: nil)) + + var mappedAddress: MediaArea.Address? + if let address { + switch address { + case let .geoPointAddress(_, countryIso2, state, city, street): + mappedAddress = MediaArea.Address( + countryIso2: countryIso2, + state: state, + city: city, + street: street + ) + } + } + + return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: nil, queryId: nil, resultId: nil), address: mappedAddress) case let .mediaAreaVenue(coordinates, geo, title, address, provider, venueId, venueType): let latitude: Double let longitude: Double @@ -505,7 +519,7 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { latitude = 0.0 longitude = 0.0 } - return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), queryId: nil, resultId: nil)) + return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), queryId: nil, resultId: nil), address: nil) case let .mediaAreaSuggestedReaction(flags, coordinates, reaction): if let reaction = MessageReaction.Reaction(apiReaction: reaction) { var parsedFlags = MediaArea.ReactionFlags() @@ -526,19 +540,80 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { } } +func apiMediaAreaFromVenue(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue, address: MediaArea.Address?) -> Api.MediaArea { + let inputCoordinates = Api.MediaAreaCoordinates.mediaAreaCoordinates(x: coordinates.x, y: coordinates.y, w: coordinates.width, h: coordinates.height, rotation: coordinates.rotation) + + if let queryId = venue.queryId, let resultId = venue.resultId { + return .inputMediaAreaVenue(coordinates: inputCoordinates, queryId: queryId, resultId: resultId) + } else if let venueInfo = venue.venue { + return .mediaAreaVenue(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), title: venueInfo.title, address: venueInfo.address ?? "", provider: venueInfo.provider ?? "", venueId: venueInfo.id ?? "", venueType: venueInfo.type ?? "") + } else { + var flags: Int32 = 0 + var mappedAddress: Api.GeoPointAddress? + if let address { + flags |= 1 << 0 + + var addressFlags: Int32 = 0 + if address.state != nil { + addressFlags |= 1 << 0 + } + if address.city != nil { + addressFlags |= 1 << 1 + } + if address.street != nil { + addressFlags |= 1 << 2 + } + + mappedAddress = .geoPointAddress( + flags: addressFlags, + countryIso2: address.countryIso2, + state: address.state, + city: address.city, + street: address.street + ) + } + return .mediaAreaGeoPoint(flags: 0, coordinates: inputCoordinates, geo: .geoPoint(flags: flags, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), address: mappedAddress) + } +} + func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transaction) -> [Api.MediaArea] { var apiMediaAreas: [Api.MediaArea] = [] for area in mediaAreas { let coordinates = area.coordinates let inputCoordinates = Api.MediaAreaCoordinates.mediaAreaCoordinates(x: coordinates.x, y: coordinates.y, w: coordinates.width, h: coordinates.height, rotation: coordinates.rotation) switch area { - case let .venue(_, venue): + case let .venue(_, venue, address): if let queryId = venue.queryId, let resultId = venue.resultId { apiMediaAreas.append(.inputMediaAreaVenue(coordinates: inputCoordinates, queryId: queryId, resultId: resultId)) } else if let venueInfo = venue.venue { apiMediaAreas.append(.mediaAreaVenue(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), title: venueInfo.title, address: venueInfo.address ?? "", provider: venueInfo.provider ?? "", venueId: venueInfo.id ?? "", venueType: venueInfo.type ?? "")) } else { - apiMediaAreas.append(.mediaAreaGeoPoint(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil))) + var flags: Int32 = 0 + var mappedAddress: Api.GeoPointAddress? + if let address { + flags |= 1 << 0 + + var addressFlags: Int32 = 0 + if address.state != nil { + addressFlags |= 1 << 0 + } + if address.city != nil { + addressFlags |= 1 << 1 + } + if address.street != nil { + addressFlags |= 1 << 2 + } + + mappedAddress = .geoPointAddress( + flags: addressFlags, + countryIso2: address.countryIso2, + state: address.state, + city: address.city, + street: address.street + ) + } + + apiMediaAreas.append(.mediaAreaGeoPoint(flags: flags, coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), address: mappedAddress)) } case let .reaction(_, reaction, flags): var apiFlags: Int32 = 0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index d388e490a6..c05c816c0e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -7,6 +7,7 @@ public enum MediaArea: Codable, Equatable { case coordinates case value case flags + case address } public struct Coordinates: Codable, Equatable { @@ -122,10 +123,25 @@ public enum MediaArea: Codable, Equatable { } } - case venue(coordinates: Coordinates, venue: Venue) + public struct Address: Codable, Equatable { + public var countryIso2: String + public var state: String? + public var city: String? + public var street: String? + + public init(countryIso2: String, state: String?, city: String?, street: String?) { + self.countryIso2 = countryIso2 + self.state = state + self.city = city + self.street = street + } + } + + case venue(coordinates: Coordinates, venue: Venue, address: Address?) case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags) case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id) case url(coordinates: Coordinates, url: String) + public struct ReactionFlags: OptionSet { public var rawValue: Int32 @@ -163,7 +179,8 @@ public enum MediaArea: Codable, Equatable { case .venue: let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let venue = try container.decode(MediaArea.Venue.self, forKey: .value) - self = .venue(coordinates: coordinates, venue: venue) + let address = try container.decodeIfPresent(MediaArea.Address.self, forKey: .address) + self = .venue(coordinates: coordinates, venue: venue, address: address) case .reaction: let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let reaction = try container.decode(MessageReaction.Reaction.self, forKey: .value) @@ -184,10 +201,11 @@ public enum MediaArea: Codable, Equatable { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case let .venue(coordinates, venue): + case let .venue(coordinates, venue, address): try container.encode(MediaAreaType.venue.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(venue, forKey: .value) + try container.encodeIfPresent(address, forKey: .address) case let .reaction(coordinates, reaction, flags): try container.encode(MediaAreaType.reaction.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) @@ -208,7 +226,7 @@ public enum MediaArea: Codable, Equatable { public extension MediaArea { var coordinates: Coordinates { switch self { - case let .venue(coordinates, _): + case let .venue(coordinates, _, _): return coordinates case let .reaction(coordinates, _, _): return coordinates diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 035bd4af42..9e785f7483 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1263,7 +1263,7 @@ public final class SearchStoryListContext: StoryListContext { public enum Source { case hashtag(String) - case venue(provider: String, id: String) + case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue, address: MediaArea.Address?) } private final class Impl { @@ -1327,8 +1327,7 @@ public final class SearchStoryListContext: StoryListContext { let accountPeerId = account.peerId var searchHashtag: String? = nil - var venueProvider: String? = nil - var venueId: String? = nil + var searchMediaArea: Api.MediaArea? var flags: Int32 = 0 switch source { @@ -1339,13 +1338,12 @@ public final class SearchStoryListContext: StoryListContext { searchHashtag = query } flags |= (1 << 0) - case .venue(let provider, let id): - venueProvider = provider - venueId = id + case let .location(coordinates, venue, address): + searchMediaArea = apiMediaAreaFromVenue(coordinates: coordinates, venue: venue, address: address) flags |= (1 << 1) } - self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, venueProvider: venueProvider, venueId: venueId, offset: "", limit: Int32(limit))) + self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: searchMediaArea, offset: loadMoreToken, limit: Int32(limit))) |> map { result -> Api.stories.FoundStories? in return result } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index aeef94f3fa..8c823e8df4 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -118,7 +118,8 @@ public enum CodableDrawingEntity: Equatable { venue: entity.location.venue, queryId: entity.queryId, resultId: entity.resultId - ) + ), + address: nil ) case let .sticker(entity): if case let .file(_, type) = entity.content, case let .reaction(reaction, style) = type { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 5983f32a6b..447b807ec8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -419,7 +419,7 @@ private final class PeerInfoPendingPane { } } - let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: key == .storyArchive, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) + let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: .peer(id: peerId, isSaved: false, isArchived: key == .storyArchive), captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index 5b58e2600a..5ba2d19928 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -28,6 +28,8 @@ swift_library( "//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/MediaEditorScreen", "//submodules/SaveToCameraRoll", + "//submodules/ShareController", + "//submodules/OpenInExternalAppUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 290b7a2620..579cfd04dd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -455,11 +455,8 @@ final class PeerInfoStoryGridScreenComponent: Component { } else { paneNode = PeerInfoStoryPaneNode( context: component.context, - peerId: component.peerId, - contentType: .photoOrVideo, + scope: .peer(id: component.peerId, isSaved: true, isArchived: component.scope == .archive), captureProtected: false, - isSaved: true, - isArchive: component.scope == .archive, isProfileEmbedded: false, canManageStories: true, navigationController: { [weak self] in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 7a8cde1f26..12d9bb4932 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -16,29 +16,24 @@ import UndoUI import MoreHeaderButton import MediaEditorScreen import SaveToCameraRoll +import ShareController +import OpenInExternalAppUI final class StorySearchGridScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let searchQuery: String + let scope: StorySearchControllerScope init( context: AccountContext, - searchQuery: String + scope: StorySearchControllerScope ) { self.context = context - self.searchQuery = searchQuery + self.scope = scope } static func ==(lhs: StorySearchGridScreenComponent, rhs: StorySearchGridScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.searchQuery != rhs.searchQuery { - return false - } - return true } @@ -99,14 +94,18 @@ final class StorySearchGridScreenComponent: Component { if let current = self.paneNode { paneNode = current } else { + let paneNodeScope: PeerInfoStoryPaneNode.Scope + switch component.scope { + case let .query(query): + paneNodeScope = .search(query: query) + case let .location(coordinates, venue, address): + paneNodeScope = .location(coordinates: coordinates, venue: venue, address: address) + } + paneNode = PeerInfoStoryPaneNode( context: component.context, - peerId: nil, - searchQuery: component.searchQuery, - contentType: .photoOrVideo, + scope: paneNodeScope, captureProtected: false, - isSaved: false, - isArchive: false, isProfileEmbedded: false, canManageStories: false, navigationController: { [weak self] in @@ -117,6 +116,7 @@ final class StorySearchGridScreenComponent: Component { }, listContext: nil ) + paneNode.parentController = environment.controller() paneNode.isEmptyUpdated = { [weak self] _ in guard let self else { return @@ -169,23 +169,31 @@ final class StorySearchGridScreenComponent: Component { } } -public class StorySearchGridScreen: ViewControllerComponentContainer { +public final class StorySearchGridScreen: ViewControllerComponentContainer { private let context: AccountContext - private let searchQuery: String + private let scope: StorySearchControllerScope private var isDismissed: Bool = false private var titleView: ChatTitleView? + override public var additionalNavigationBarHeight: CGFloat { + if let componentView = self.node.hostView.componentView as? StorySearchGridScreenComponent.View, let paneNode = componentView.paneNode { + return paneNode.additionalNavigationHeight + } else { + return 0.0 + } + } + public init( context: AccountContext, - searchQuery: String + scope: StorySearchControllerScope ) { self.context = context - self.searchQuery = searchQuery + self.scope = scope super.init(context: context, component: StorySearchGridScreenComponent( context: context, - searchQuery: searchQuery + scope: scope ), navigationBarAppearance: .default, theme: .default) let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) @@ -203,6 +211,10 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { self.navigationItem.titleView = self.titleView + if case .location = scope { + self.navigationItem.setRightBarButton(UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed)), animated: true) + } + self.updateTitle() self.scrollToTop = { [weak self] in @@ -220,6 +232,28 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { deinit { } + @objc private func sharePressed() { + guard case let .location(_, venue, _) = self.scope else { + return + } + let locationMap = TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + + let shareAction = OpenInControllerAction(title: presentationData.strings.Conversation_ContextMenuShare, action: { [weak self] in + guard let self else { + return + } + self.present(ShareController(context: self.context, subject: .mapMedia(locationMap), externalShare: true), in: .window(.root), with: nil) + }) + self.present(OpenInActionSheetController(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in + guard let self else { + return + } + self.context.sharedContext.applicationBindings.openUrl(url) + }), in: .window(.root), with: nil) + } + func updateTitle() { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let _ = presentationData @@ -236,7 +270,12 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { title = nil } //TODO:localize - self.titleView?.titleContent = .custom("\(self.searchQuery)", title, false) + switch self.scope { + case let .query(query): + self.titleView?.titleContent = .custom("\(query)", title, false) + case .location: + self.titleView?.titleContent = .custom("Location", nil, false) + } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 987dbc1b7b..aa61d9591a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -44,6 +44,8 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramUI/Components/MediaEditorScreen", + "//submodules/LocationUI", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 63390d65a7..5690d3832c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -35,6 +35,11 @@ import PlainButtonComponent import ComponentDisplayAdapters import MediaEditorScreen import AvatarNode +import LocationUI +import CoreLocation +import Geocoding +import ItemListUI +import MultilineTextComponent private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -1064,6 +1069,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { var onTagTapImpl: (() -> Void)? var didScrollImpl: (() -> Void)? var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? + var scrollingOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? @@ -1333,6 +1339,10 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) { self.coveringInsetOffsetUpdatedImpl?(transition) } + + func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + self.scrollingOffsetUpdatedImpl?(transition) + } func onBeginFastScrolling() { self.onBeginFastScrollingImpl?() @@ -1347,13 +1357,97 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } } -public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { - public enum ContentType { - case photoOrVideo - case photo - case video +private final class StorySearchHeaderComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let count: Int + + init( + theme: PresentationTheme, + strings: PresentationStrings, + count: Int + ) { + self.theme = theme + self.strings = strings + self.count = count } + + static func ==(lhs: StorySearchHeaderComponent, rhs: StorySearchHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.count != rhs.count { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + + private var component: StorySearchHeaderComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorySearchHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + self.backgroundColor = component.theme.chatList.sectionHeaderFillColor + } + + let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0) + + //TODO:localize + let titleString: String + if component.count == 1 { + titleString = "1 STORY FROM THIS LOCATION" + } else { + titleString = "\(component.count) STORIES FROM THIS LOCATION" + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.regular(13.0), textColor: component.theme.chatList.sectionHeaderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - insets.left - insets.right, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize) + } + + return CGSize(width: availableSize.width, height: titleSize.height + insets.top + insets.bottom) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} +public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { + public enum Scope { + case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) + case search(query: String) + case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue, address: MediaArea.Address?) + } + public struct ZoomLevel { fileprivate var value: SparseItemGrid.ZoomLevel @@ -1370,26 +1464,66 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } + private struct MapInfoData: Equatable { + var location: TelegramMediaMap + var address: String? + var distance: Double? + var drivingTime: ExpectedTravelTime + var transitTime: ExpectedTravelTime + var walkingTime: ExpectedTravelTime + var hasEta: Bool + + init( + location: TelegramMediaMap, + address: String?, + distance: Double?, + drivingTime: ExpectedTravelTime, + transitTime: ExpectedTravelTime, + walkingTime: ExpectedTravelTime, + hasEta: Bool + ) { + self.location = location + self.address = address + self.distance = distance + self.drivingTime = drivingTime + self.transitTime = transitTime + self.walkingTime = walkingTime + self.hasEta = hasEta + } + } + private let context: AccountContext - private let peerId: PeerId? - private let searchQuery: String? - private let isSaved: Bool - private let isArchive: Bool + private let scope: Scope private let isProfileEmbedded: Bool private let canManageStories: Bool - public private(set) var contentType: ContentType - private var contentTypePromise: ValuePromise private let navigationController: () -> NavigationController? public weak var parentController: ViewController? private let contextGestureContainerNode: ContextControllerSourceNode + + private var mapOptionsNode: LocationOptionsNode? + private var mapNode: LocationMapHeaderNode? + private var mapDisposable: Disposable? + + private var locationViewState: LocationViewState = LocationViewState() { + didSet { + self.locationViewStatePromise.set(.single(self.locationViewState)) + } + } + private let locationViewStatePromise = Promise(LocationViewState()) + + private var mapInfoData: MapInfoData? + private var mapInfoNode: LocationInfoListItemNode? + private var searchHeader: ComponentView? + private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var pinnedIds: Set = Set() + private var itemCount: Int? private var didUpdateItemsOnce: Bool = false private var selectionPanel: ComponentView? @@ -1490,20 +1624,27 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private weak var contextControllerToDismissOnSelection: ContextControllerProtocol? private weak var tempContextContentItemNode: TempExtractedItemNode? + + public var additionalNavigationHeight: CGFloat { + if self.locationViewState.displayingMapModeOptions { + return 38.0 + } + return 0.0 + } - public init(context: AccountContext, peerId: PeerId?, searchQuery: String? = nil, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: StoryListContext?) { + public init(context: AccountContext, scope: Scope, captureProtected: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: StoryListContext?) { self.context = context - self.peerId = peerId - self.searchQuery = searchQuery - self.contentType = contentType - self.contentTypePromise = ValuePromise(contentType) + self.scope = scope self.navigationController = navigationController - self.isSaved = isSaved - self.isArchive = isArchive self.isProfileEmbedded = isProfileEmbedded self.canManageStories = canManageStories - self.isSelectionModeActive = !isProfileEmbedded && isArchive + switch scope { + case let .peer(_, _, isArchived): + self.isSelectionModeActive = !isProfileEmbedded && isArchived + default: + self.isSelectionModeActive = false + } self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -1520,16 +1661,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let listContext { self.listSource = listContext - } else if let searchQuery { - self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(searchQuery)) } else { - self.listSource = PeerStoryListContext(account: context.account, peerId: peerId ?? context.account.peerId, isArchived: self.isArchive) + switch self.scope { + case let .peer(id, _, isArchived): + self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived) + case let .search(query): + self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(query)) + case let .location(coordinates, venue, address): + self.listSource = SearchStoryListContext(account: context.account, source: .location(coordinates: coordinates, venue: venue, address: address)) + } } self.calendarSource = nil super.init() - if self.peerId != nil { + if case .peer = self.scope { let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { @@ -1577,9 +1723,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } //TODO:localize + let listPeerId: EnginePeer.Id + switch self.scope { + case let .peer(id, _, _): + listPeerId = id + default: + listPeerId = self.context.account.peerId + } let listContext = PeerStoryListContentContextImpl( context: self.context, - peerId: self.peerId ?? self.context.account.peerId, + peerId: listPeerId, listContext: self.listSource, initialId: item.story.id ) @@ -1732,15 +1885,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } strongSelf.paneDidScroll?() - strongSelf.cancelPreviewGestures() + + if strongSelf.locationViewState.displayingMapModeOptions { + strongSelf.locationViewState.displayingMapModeOptions = false + strongSelf.parentController?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } } self.itemGridBinding.coveringInsetOffsetUpdatedImpl = { [weak self] transition in - guard let strongSelf = self else { + guard let self else { return } - strongSelf.tabBarOffsetUpdated?(transition) + self.tabBarOffsetUpdated?(transition) + } + + self.itemGridBinding.scrollingOffsetUpdatedImpl = { [weak self] transition in + guard let self else { + return + } + self.gridScrollingOffsetUpdated(transition: transition) } var processedOnBeginFastScrolling = false @@ -1841,7 +2005,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } ) - //TODO:selection if self.isSelectionModeActive { self._itemInteraction?.selectedIds = Set() } @@ -1850,6 +2013,41 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded self.contextGestureContainerNode.addSubnode(self.itemGrid) self.addSubnode(self.contextGestureContainerNode) + + if case .location = scope { + let mapNode = LocationMapHeaderNode( + presentationData: self.presentationData, + toggleMapModeSelection: { [weak self] in + guard let self else { + return + } + + var state = self.locationViewState + state.displayingMapModeOptions = !state.displayingMapModeOptions + self.locationViewState = state + }, + goToUserLocation: { [weak self] in + guard let self else { + return + } + + var state = self.locationViewState + state.displayingMapModeOptions = false + state.selectedLocation = .user + switch state.trackingMode { + case .none: + state.trackingMode = .follow + case .follow: + state.trackingMode = .followWithHeading + case .followWithHeading: + state.trackingMode = .none + } + self.locationViewState = state + } + ) + self.mapNode = mapNode + self.addSubnode(mapNode) + } self.contextGestureContainerNode.shouldBegin = { [weak self] point in guard let strongSelf = self else { @@ -1939,7 +2137,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr strongSelf.openContextMenu(item: story, itemLayer: itemLayer, rect: rect, gesture: gesture) } - self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: self.isArchive ? .storyArchive : .stories))) + let paneKey: PeerInfoPaneKey + switch self.scope { + case let .peer(_, _, isArchived): + paneKey = isArchived ? .storyArchive : .stories + default: + paneKey = .stories + } + self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: paneKey))) self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -1948,15 +2153,147 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData) - strongSelf.itemGrid.updatePresentationData(theme: presentationData.theme) + strongSelf.mapOptionsNode?.updatePresentationData(presentationData) }) self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) - if peerId == context.account.peerId && !isArchive { + if case let .peer(id, _, isArchived) = self.scope, id == context.account.peerId, !isArchived { self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: context.account.peerId, isArchived: true) } + + if case let .location(_, venue, _) = scope, let mapNode = self.mapNode { + let locationCoordinate = CLLocationCoordinate2D(latitude: venue.latitude, longitude: venue.longitude) + + var initialMapState = LocationViewState() + initialMapState.selectedLocation = .coordinate(locationCoordinate, true) + self.locationViewStatePromise.set(.single(initialMapState)) + + let userLocation: Signal = .single(nil) + |> then( + throttledUserLocation(mapNode.mapNode.userLocation) + ) + + var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) + var address: Signal = .single(nil) + + let locale = localeWithStrings(self.presentationData.strings) + eta = .single((.calculating, .calculating, .calculating)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .transit), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) + |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + if case .calculating = drivingTime { + return .complete() + } + if case .calculating = transitTime { + return .complete() + } + if case .calculating = walkingTime { + return .complete() + } + + return .single((drivingTime, transitTime, walkingTime)) + }) + + /*if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { + address = .single(venueAddress) + } else*/ do { + address = .single(nil) + |> then( + reverseGeocodeLocation(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude, locale: locale) + |> map { placemark -> String? in + return placemark?.compactDisplayAddress ?? "" + } + ) + } + + let previousState = Atomic(value: nil) + let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) + + self.mapDisposable = (combineLatest( + context.sharedContext.presentationData, + self.locationViewStatePromise.get(), + mapNode.mapNode.userLocation, + userLocation, + address, + eta + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, distance, address, eta in + guard let self, let mapNode = self.mapNode else { + return + } + + let previousState = previousState.swap(state) + + var annotations: [LocationPinAnnotation] = [] + + let subjectLocation = CLLocation(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude) + let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } + + let locationMap = TelegramMediaMap(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + + let mapInfoData = MapInfoData( + location: locationMap, + address: address, + distance: distance, + drivingTime: eta.0, + transitTime: eta.1, + walkingTime: eta.2, + hasEta: false + ) + + annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: locationMap, queryId: nil, resultId: nil, forcedSelection: true)) + + mapNode.updateState( + mapMode: state.mapMode, + trackingMode: state.trackingMode, + displayingMapModeOptions: state.displayingMapModeOptions, + displayingPlacesButton: false, + proximityNotification: nil, + animated: false + ) + + mapNode.mapNode.trackingMode = state.trackingMode + + let previousAnnotations = previousAnnotations.swap(annotations) + if annotations != previousAnnotations { + mapNode.mapNode.annotations = annotations + } + + switch state.selectedLocation { + case .initial: + if previousState?.selectedLocation != .initial { + mapNode.mapNode.setMapCenter(coordinate: locationCoordinate, span: LocationMapNode.viewMapSpan, animated: previousState != nil) + } + case let .coordinate(coordinate, defaultSpan): + if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, previousCoordinate == coordinate { + } else { + mapNode.mapNode.setMapCenter( + coordinate: coordinate, + span: defaultSpan ? LocationMapNode.defaultMapSpan : LocationMapNode.viewMapSpan, + animated: true + ) + } + case .user: + if previousState?.selectedLocation != .user, let userLocation = userLocation { + mapNode.mapNode.setMapCenter( + coordinate: userLocation.coordinate, + isUserLocation: true, + animated: true + ) + } + case .custom: + break + } + + if self.mapInfoData != mapInfoData { + self.mapInfoData = mapInfoData + self.update(transition: .immediate) + } else if let previousState, previousState.displayingMapModeOptions != state.displayingMapModeOptions { + self.parentController?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + }) + } } deinit { @@ -1965,6 +2302,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.animationTimer?.invalidate() self.presentationDataDisposable?.dispose() self.updateDisposable.dispose() + self.mapDisposable?.dispose() } public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { @@ -1991,24 +2329,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var items: [ContextMenuItem] = [] - if canManage, let peerId = self.peerId { - items.append(.action(ContextMenuActionItem(text: !self.isArchive ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: self.isArchive ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + if canManage, case let .peer(peerId, _, isArchived) = self.scope { + items.append(.action(ContextMenuActionItem(text: !isArchived ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in guard let self else { f(.default) return } - if self.isArchive { + if isArchived { f(.default) } else { f(.dismissWithoutContent) } - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: [item.id: item], isPinned: self.isArchive ? true : false).startStandalone() - self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.isArchive ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: [item.id: item], isPinned: isArchived ? true : false).startStandalone() + self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: isArchived ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) - if !self.isArchive { + if !isArchived { let isPinned = self.pinnedIds.contains(item.id) items.append(.action(ContextMenuActionItem(text: isPinned ? self.presentationData.strings.StoryList_ItemAction_Unpin : self.presentationData.strings.StoryList_ItemAction_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in itemLayer?.isHidden = false @@ -2104,7 +2442,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if !item.isForwardingDisabled, case .everyone = item.privacy?.base { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { - guard let self, let peerId = self.peerId else { + guard let self, case let .peer(peerId, _, _) = self.scope else { return } @@ -2206,9 +2544,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr parentController.presentInGlobalOverlay(contextController) } - public func updateContentType(contentType: ContentType) { - } - public func updateZoomLevel(level: ZoomLevel) { self.itemGrid.setZoomLevel(level: level.value) @@ -2257,16 +2592,25 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let title: String if state.totalCount == 0 { title = "" - } else { - if self.isSaved { + } else if case let .peer(_, isSaved, isArchived) = self.scope { + if isSaved { title = self.presentationData.strings.StoryList_SubtitleSaved(Int32(state.totalCount)) - } else if self.isArchive { + } else if isArchived { title = self.presentationData.strings.StoryList_SubtitleArchived(Int32(state.totalCount)) } else { title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } + } else { + title = "" } - self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: self.isArchive ? .storyArchive : .stories))) + let paneKey: PeerInfoPaneKey + switch self.scope { + case let .peer(_, _, isArchived): + paneKey = isArchived ? .storyArchive : .stories + default: + paneKey = .stories + } + self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: paneKey))) let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) @@ -2309,8 +2653,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } var headerText: String? - if strongSelf.isArchive && !mappedItems.isEmpty && strongSelf.peerId == strongSelf.context.account.peerId { - headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription + if case let .peer(peerId, _, isArchived) = strongSelf.scope { + if isArchived && !mappedItems.isEmpty && peerId == strongSelf.context.account.peerId { + headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription + } } let items = SparseItemGrid.Items( @@ -2321,6 +2667,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr headerText: headerText, snapTopInset: false ) + + strongSelf.itemCount = state.totalCount let currentSynchronous = synchronous && firstTime let currentReloadAtTop = reloadAtTop && firstTime @@ -2649,7 +2997,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func presentDeleteConfirmation(ids: Set) { - guard let peerId = self.peerId else { + guard case let .peer(peerId, _, _) = self.scope else { return } @@ -2683,11 +3031,187 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } + private func update(transition: ContainedViewLayoutTransition) { + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition) + } + } + + private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + if let _ = self.mapNode, let currentParams = self.currentParams { + self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) + } + } + + private var effectiveMapHeight: CGFloat = 0.0 + private func updateMapLayout(size: CGSize, topInset: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { + guard let mapNode = self.mapNode else { + return + } + + let mapOverscrollInset: CGFloat = 200.0 + + var mapHeight = min(size.width, size.height) + mapHeight = min(mapHeight, floor(size.height * 0.389)) + + self.effectiveMapHeight = mapHeight - self.additionalNavigationHeight + let mapSize = CGSize(width: size.width, height: mapHeight + mapOverscrollInset) + + let mapFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - mapOverscrollInset - self.itemGrid.scrollingOffset - self.additionalNavigationHeight), size: mapSize) + transition.updateFrame(node: mapNode, frame: mapFrame) + + mapNode.updateLayout( + layout: ContainerViewLayout( + size: mapSize, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), + deviceMetrics: deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: mapOverscrollInset, left: 0.0, bottom: 0.0, right: 0.0), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ), + navigationBarHeight: 0.0, + topPadding: mapOverscrollInset + self.additionalNavigationHeight, + offset: min(floorToScreenPixels(self.itemGrid.scrollingOffset * 0.5), mapSize.height), + size: mapSize, + transition: transition + ) + + if let mapInfoData = self.mapInfoData { + let mapInfoNode: LocationInfoListItemNode + if let current = self.mapInfoNode { + mapInfoNode = current + } else { + mapInfoNode = LocationInfoListItemNode() + mapInfoNode.isUserInteractionEnabled = false + self.mapInfoNode = mapInfoNode + mapNode.supernode?.insertSubnode(mapInfoNode, aboveSubnode: mapNode) + } + + let addressString: String? + if let address = mapInfoData.address { + addressString = address + } else { + addressString = self.presentationData.strings.Map_Locating + } + let distanceString: String? + if let distance = mapInfoData.distance { + distanceString = distance < 10 ? self.presentationData.strings.Map_YouAreHere : self.presentationData.strings.Map_DistanceAway(stringForDistance(strings: self.presentationData.strings, distance: distance)).string + } else { + distanceString = nil + } + + let item = LocationInfoListItem( + presentationData: ItemListPresentationData(self.presentationData), + engine: self.context.engine, + location: mapInfoData.location, + address: addressString, + distance: distanceString, + drivingTime: mapInfoData.drivingTime, + transitTime: mapInfoData.transitTime, + walkingTime: mapInfoData.walkingTime, + hasEta: mapInfoData.hasEta, + action: {}, + drivingAction: {}, + transitAction: {}, + walkingAction: {} + ) + let (mapInfoLayout, mapInfoReadyAndApply) = mapInfoNode.asyncLayout()( + item, + ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0, isStandalone: true) + ) + + let mapInfoTopInset: CGFloat = -6.0 + + let mapInfoFrame = CGRect(origin: CGPoint(x: 0.0, y: mapFrame.maxY + mapInfoTopInset), size: mapInfoLayout.contentSize) + mapInfoNode.frame = mapInfoFrame + mapInfoReadyAndApply().1(ListViewItemApply(isOnScreen: true)) + + self.effectiveMapHeight += mapInfoLayout.contentSize.height + mapInfoTopInset + + if let itemCount = self.itemCount, itemCount != 0 { + let searchHeader: ComponentView + if let current = self.searchHeader { + searchHeader = current + } else { + searchHeader = ComponentView() + self.searchHeader = searchHeader + } + let searchHeaderSize = searchHeader.update( + transition: Transition(transition), + component: AnyComponent(StorySearchHeaderComponent( + theme: self.presentationData.theme, + strings: self.presentationData.strings, + count: itemCount + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 1000.0) + ) + let searchHeaderFrame = CGRect(origin: CGPoint(x: 0.0, y: max(topInset, mapInfoFrame.maxY)), size: searchHeaderSize) + if let searchHeaderView = searchHeader.view { + if searchHeaderView.superview == nil { + self.view.addSubview(searchHeaderView) + } + transition.updateFrame(view: searchHeaderView, frame: searchHeaderFrame) + } + self.effectiveMapHeight += searchHeaderSize.height + } else { + if let searchHeader = self.searchHeader { + self.searchHeader = nil + searchHeader.view?.removeFromSuperview() + } + } + } else { + if let mapInfoNode = self.mapInfoNode { + self.mapInfoNode = nil + mapInfoNode.removeFromSupernode() + } + if let searchHeader = self.searchHeader { + self.searchHeader = nil + searchHeader.view?.removeFromSuperview() + } + } + } + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) + var gridTopInset = topInset + + if self.mapNode != nil { + self.updateMapLayout(size: size, topInset: topInset, deviceMetrics: deviceMetrics, transition: transition) + gridTopInset += self.effectiveMapHeight + + let mapOptionsNode: LocationOptionsNode + if let current = self.mapOptionsNode { + mapOptionsNode = current + } else { + mapOptionsNode = LocationOptionsNode(presentationData: self.presentationData, hasBackground: false, updateMapMode: { [weak self] mode in + guard let self else { + return + } + + var state = self.locationViewState + state.mapMode = mode + state.displayingMapModeOptions = false + self.locationViewState = state + }) + mapOptionsNode.clipsToBounds = true + self.mapOptionsNode = mapOptionsNode + self.parentController?.navigationBar?.additionalContentNode.addSubnode(mapOptionsNode) + } + + let mapOptionsFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - self.additionalNavigationHeight), size: CGSize(width: size.width, height: self.additionalNavigationHeight)) + transition.updatePosition(node: mapOptionsNode, position: mapOptionsFrame.center) + transition.updateBounds(node: mapOptionsNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: 38.0 - self.additionalNavigationHeight), size: mapOptionsFrame.size)) + mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) + } + var bottomInset = bottomInset - if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, let peerId = self.peerId { + if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = Transition(transition) if let current = self.selectionPanel { @@ -2757,7 +3281,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionItems.append(BottomActionsPanelComponent.Item( id: "archive", color: .accent, - title: self.isArchive ? presentationData.strings.StoryList_ActionPanel_Unarchive : presentationData.strings.StoryList_ActionPanel_Archive, + title: isArchived ? presentationData.strings.StoryList_ActionPanel_Unarchive : presentationData.strings.StoryList_ActionPanel_Archive, isEnabled: !selectedIds.isEmpty, action: { [weak self] in guard let self, let _ = self.itemInteraction.selectedIds else { @@ -2770,10 +3294,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr parentController.cancelItemSelection() } - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: items, isPinned: self.isArchive ? true : false).startStandalone() + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: items, isPinned: isArchived ? true : false).startStandalone() let text: String - if self.isArchive { + if isArchived { text = presentationData.strings.StoryList_ToastUnarchived_Text(Int32(items.count)) } else { text = presentationData.strings.StoryList_ToastArchived_Text(Int32(items.count)) @@ -2823,7 +3347,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - if self.searchQuery == nil, let items = self.items, items.items.isEmpty, items.count == 0 { + if case let .peer(_, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { let emptyStateView: ComponentView var emptyStateTransition = Transition(transition) if let current = self.emptyStateView { @@ -2840,16 +3364,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, animationName: "StoryListEmpty", - title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, - text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, - actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction, + title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, + text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, + actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction, action: { [weak self] in guard let self else { return } self.emptyAction?() }, - additionalActionTitle: (self.isArchive || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, additionalAction: { [weak self] in guard let self else { return @@ -2858,14 +3382,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } )), environment: {}, - containerSize: CGSize(width: size.width, height: size.height - topInset - bottomInset) + containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) ) let emptyStateFrame: CGRect if self.isProfileEmbedded { - emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(topInset, floor((visibleHeight - topInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) } else { - emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: topInset), size: emptyStateSize) + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) } if let emptyStateComponentView = emptyStateView.view { @@ -2917,14 +3441,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.didUpdateItemsOnce = true let fixedItemHeight: CGFloat? let isList = false - switch self.contentType { - default: - fixedItemHeight = nil - } + fixedItemHeight = nil let fixedItemAspect: CGFloat? = 0.81 - - let gridTopInset = topInset self.itemGrid.pinchEnabled = items.count > 2 self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index a9be603144..e9acd3ea77 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1014,6 +1014,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) { self.coveringInsetOffsetUpdatedImpl?(transition) } + + func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + } func onBeginFastScrolling() { self.onBeginFastScrollingImpl?() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index e5ce122d6e..183c64b50f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2919,7 +2919,7 @@ final class StoryItemSetContainerSendMessage { } if !hashtag.isEmpty { if peerName == nil { - let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, query: hashtag) + let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(hashtag)) navigationController.pushViewController(searchController) } else { let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) @@ -3346,9 +3346,10 @@ final class StoryItemSetContainerSendMessage { var actions: [ContextMenuAction] = [] switch mediaArea { - case let .venue(_, venue): + case let .venue(coordinates, venue, address): let action = { [weak controller, weak view] in - let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let _ = view + /*let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let locationController = LocationViewController( context: context, updatedPresentationData: updatedPresentationData, @@ -3370,7 +3371,10 @@ final class StoryItemSetContainerSendMessage { view?.updateIsProgressPaused() }) } - controller?.push(locationController) + controller?.push(locationController)*/ + + let searchController = context.sharedContext.makeStorySearchController(context: context, scope: .location(coordinates: coordinates, venue: venue, address: address)) + controller?.push(searchController) } if immediate { action() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3501a5e786..e085c6802c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1908,8 +1908,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return HashtagSearchController(context: context, peer: peer, query: query, all: all) } - public func makeStorySearchController(context: AccountContext, query: String) -> ViewController { - return StorySearchGridScreen(context: context, searchQuery: query) + public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope) -> ViewController { + return StorySearchGridScreen(context: context, scope: scope) } public func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController {