import Foundation import UIKit import Display import SwiftSignalKit import LegacyComponents import TelegramCore import SyncCore import Postbox import TelegramPresentationData import AccountContext import ShareController import LegacyUI import OpenInExternalAppUI import AppBundle import LocationResources import DeviceLocationManager private func generateClearIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) } func makeLegacyPeer(_ peer: Peer) -> AnyObject? { if let user = peer as? TelegramUser { let legacyUser = TGUser() legacyUser.uid = user.id.id legacyUser.firstName = user.firstName legacyUser.lastName = user.lastName if let representation = smallestImageRepresentation(user.photo) { legacyUser.photoUrlSmall = legacyImageLocationUri(resource: representation.resource) } return legacyUser } else if let channel = peer as? TelegramChannel { let legacyConversation = TGConversation() legacyConversation.conversationId = Int64(channel.id.id) legacyConversation.chatTitle = channel.title if let representation = smallestImageRepresentation(channel.photo) { legacyConversation.chatPhotoSmall = legacyImageLocationUri(resource: representation.resource) } return legacyConversation } else { return nil } } private func makeLegacyMessage(_ message: Message) -> TGMessage { let result = TGMessage() result.mid = message.id.id result.date = Double(message.timestamp) if message.flags.contains(.Failed) { result.deliveryState = TGMessageDeliveryStateFailed } else if message.flags.contains(.Sending) { result.deliveryState = TGMessageDeliveryStatePending } else { result.deliveryState = TGMessageDeliveryStateDelivered } for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { result.editDate = Double(attribute.date) } } var media: [Any] = [] for m in message.media { if let mapMedia = m as? TelegramMediaMap { let legacyLocation = TGLocationMediaAttachment() legacyLocation.latitude = mapMedia.latitude legacyLocation.longitude = mapMedia.longitude if let venue = mapMedia.venue { legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id, type: venue.type) } if let liveBroadcastingTimeout = mapMedia.liveBroadcastingTimeout { legacyLocation.period = liveBroadcastingTimeout } media.append(legacyLocation) } } if !media.isEmpty { result.mediaAttachments = media } return result } private func legacyRemainingTime(message: TGMessage) -> SSignal { var liveBroadcastingTimeoutValue: Int32? if let mediaAttachments = message.mediaAttachments { for media in mediaAttachments { if let m = media as? TGLocationMediaAttachment, m.period != 0 { liveBroadcastingTimeoutValue = m.period } } } guard let liveBroadcastingTimeout = liveBroadcastingTimeoutValue else { return SSignal.fail(nil) } if message.deliveryState != TGMessageDeliveryStateDelivered { return SSignal.single(liveBroadcastingTimeout as NSNumber) } let remainingTime = SSignal.`defer`({ let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let remainingTime = max(0, Int32(message.date) + liveBroadcastingTimeout - currentTime) var signal = SSignal.single(remainingTime as NSNumber) if remainingTime == 0 { signal = signal?.then(SSignal.fail(nil)) } return signal })! return (remainingTime.then(SSignal.complete().delay(5.0, on: SQueue.main()))).restart().`catch`({ _ in return SSignal.complete() }) } private func telegramMap(for location: TGLocationMediaAttachment) -> TelegramMediaMap { let mapVenue: MapVenue? if let venue = location.venue { mapVenue = MapVenue(title: venue.title ?? "", address: venue.address ?? "", provider: venue.provider ?? "", id: venue.venueId ?? "", type: venue.type ?? "") } else { mapVenue = nil } return TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: mapVenue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) } func legacyLocationPalette(from theme: PresentationTheme) -> TGLocationPallete { let listTheme = theme.list let searchTheme = theme.rootController.navigationSearchBar return TGLocationPallete(backgroundColor: listTheme.plainBackgroundColor, selectionColor: listTheme.itemHighlightedBackgroundColor, separatorColor: listTheme.itemPlainSeparatorColor, textColor: listTheme.itemPrimaryTextColor, secondaryTextColor: listTheme.itemSecondaryTextColor, accentColor: listTheme.itemAccentColor, destructiveColor: listTheme.itemDestructiveColor, locationColor: UIColor(rgb: 0x008df2), liveLocationColor: UIColor(rgb: 0xff6464), iconColor: searchTheme.backgroundColor, sectionHeaderBackgroundColor: theme.chatList.sectionHeaderFillColor, sectionHeaderTextColor: theme.chatList.sectionHeaderTextColor, searchBarPallete: TGSearchBarPallete(dark: theme.overallDarkAppearance, backgroundColor: searchTheme.backgroundColor, highContrastBackgroundColor: searchTheme.backgroundColor, textColor: searchTheme.inputTextColor, placeholderColor: searchTheme.inputPlaceholderTextColor, clearIcon: generateClearIcon(color: theme.rootController.navigationSearchBar.inputClearButtonColor), barBackgroundColor: searchTheme.backgroundColor, barSeparatorColor: searchTheme.separatorColor, plainBackgroundColor: searchTheme.backgroundColor, accentColor: searchTheme.accentColor, accentContrastColor: searchTheme.backgroundColor, menuBackgroundColor: searchTheme.backgroundColor, segmentedControlBackgroundImage: nil, segmentedControlSelectedImage: nil, segmentedControlHighlightedImage: nil, segmentedControlDividerImage: nil), avatarPlaceholder: nil) } public func legacyLocationController(message: Message?, mapMedia: TelegramMediaMap, context: AccountContext, openPeer: @escaping (Peer) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, stopLiveLocation: @escaping () -> Void, openUrl: @escaping (String) -> Void) -> ViewController { let legacyLocation = TGLocationMediaAttachment() legacyLocation.latitude = mapMedia.latitude legacyLocation.longitude = mapMedia.longitude if let venue = mapMedia.venue { legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id, type: venue.type) } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .navigation, theme: presentationData.theme, strings: presentationData.strings) legacyController.navigationPresentation = .modal let controller: TGLocationViewController let venueColor = mapMedia.venue?.type.flatMap { venueIconColor(type: $0) } if let message = message { let legacyMessage = makeLegacyMessage(message) let legacyAuthor: AnyObject? = message.author.flatMap(makeLegacyPeer) let updatedLocations = SSignal(generator: { subscriber in let disposable = topPeerActiveLiveLocationMessages(viewTracker: context.account.viewTracker, accountPeerId: context.account.peerId, peerId: message.id.peerId).start(next: { (_, messages) in var result: [Any] = [] let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) loop: for message in messages { var liveBroadcastingTimeout: Int32 = 0 mediaLoop: for media in message.media { if let map = media as? TelegramMediaMap, let timeout = map.liveBroadcastingTimeout { liveBroadcastingTimeout = timeout break mediaLoop } } let legacyMessage = makeLegacyMessage(message) guard let legacyAuthor = message.author.flatMap(makeLegacyPeer) else { continue loop } let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) if legacyMessage.locationAttachment?.period != 0 { let hasOwnSession = message.localTags.contains(.OutgoingLiveLocation) var isOwn = false if !message.flags.contains(.Incoming) { isOwn = true } else if let peer = message.peers[message.id.peerId] as? TelegramChannel { isOwn = peer.hasPermission(.sendMessages) } let liveLocation = TGLiveLocation(message: legacyMessage, peer: legacyAuthor, hasOwnSession: hasOwnSession, isOwnLocation: isOwn, isExpired: remainingTime == 0)! result.append(liveLocation) } } subscriber?.putNext(result) }) return SBlockDisposable(block: { disposable.dispose() }) })! if let liveBroadcastingTimeout = mapMedia.liveBroadcastingTimeout { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) let messageLiveLocation = TGLiveLocation(message: legacyMessage, peer: legacyAuthor, hasOwnSession: false, isOwnLocation: false, isExpired: remainingTime == 0)! controller = TGLocationViewController(context: legacyController.context, liveLocation: messageLiveLocation) if remainingTime == 0 { let freezeLocations: [Any] = [messageLiveLocation] controller.setLiveLocationsSignal(.single(freezeLocations)) } else { controller.setLiveLocationsSignal(updatedLocations) if message.flags.contains(.Incoming) { context.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: Set([message.id])) } } } else { controller = TGLocationViewController(context: legacyController.context, message: legacyMessage, peer: legacyAuthor, color: venueColor)! controller.receivingPeer = message.peers[message.id.peerId].flatMap(makeLegacyPeer) controller.setLiveLocationsSignal(updatedLocations) } let namespacesWithEnabledLiveLocation: Set = Set([ Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudUser ]) if namespacesWithEnabledLiveLocation.contains(message.id.peerId.namespace) { controller.allowLiveLocationSharing = true } } else { let attachment = TGLocationMediaAttachment() attachment.latitude = mapMedia.latitude attachment.longitude = mapMedia.longitude controller = TGLocationViewController(context: legacyController.context, locationAttachment: attachment, peer: nil, color: venueColor) } controller.remainingTimeForMessage = { message in return legacyRemainingTime(message: message!) } controller.liveLocationStarted = { [weak legacyController] coordinate, period in sendLiveLocation(coordinate, period) legacyController?.dismiss() } controller.liveLocationStopped = { [weak legacyController] in stopLiveLocation() legacyController?.dismiss() } let theme = (context.sharedContext.currentPresentationData.with { $0 }).theme controller.pallete = legacyLocationPalette(from: theme) controller.modalMode = true controller.presentActionsMenu = { [weak legacyController] legacyLocation, directions in if let strongLegacyController = legacyController, let location = legacyLocation { let map = telegramMap(for: location) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let shareAction = OpenInControllerAction(title: presentationData.strings.Conversation_ContextMenuShare, action: { strongLegacyController.present(ShareController(context: context, subject: .mapMedia(map), externalShare: true), in: .window(.root), with: nil) }) strongLegacyController.present(OpenInActionSheetController(context: context, item: .location(location: map, withDirections: directions), additionalAction: !directions ? shareAction : nil, openUrl: openUrl), in: .window(.root), with: nil) } } controller.onViewDidAppear = { [weak controller] in if let strongController = controller { strongController.view.disablesInteractiveModalDismiss = true strongController.view.disablesInteractiveTransitionGestureRecognizer = true strongController.locationMapView.interactiveTransitionGestureRecognizerTest = { point -> Bool in return point.x > 30.0 } } } legacyController.navigationItem.title = controller.navigationItem.title controller.updateRightBarItem = { item, action, animated in if action { legacyController.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(theme), style: .plain, target: controller, action: #selector(controller.actionsButtonPressed)) } else { legacyController.navigationItem.setRightBarButton(item, animated: animated) } } legacyController.bind(controller: controller) let presentationDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in if let controller = controller { controller.pallete = legacyLocationPalette(from: presentationData.theme) } }) legacyController.disposables.add(presentationDisposable) return legacyController }