diff --git a/Rocket.Chat.xcodeproj/project.pbxproj b/Rocket.Chat.xcodeproj/project.pbxproj index 61ddeb225e..d2cb508b22 100644 --- a/Rocket.Chat.xcodeproj/project.pbxproj +++ b/Rocket.Chat.xcodeproj/project.pbxproj @@ -537,6 +537,14 @@ 805DEC351FFC03380033151B /* CustomEmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805DEC341FFC03380033151B /* CustomEmojiManager.swift */; }; 805DEC371FFC08870033151B /* CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805DEC361FFC08870033151B /* CustomEmoji.swift */; }; 805DEC391FFE54820033151B /* CustomEmojiSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805DEC381FFE54820033151B /* CustomEmojiSpec.swift */; }; + 80607DDF2232306800B84D91 /* SharedLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80607DDB2232306800B84D91 /* SharedLocationViewController.swift */; }; + 80607DE02232306800B84D91 /* LocationShareDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80607DDC2232306800B84D91 /* LocationShareDelegate.swift */; }; + 80607DE12232306800B84D91 /* LocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80607DDD2232306800B84D91 /* LocationViewController.swift */; }; + 80607DE22232306800B84D91 /* LocationPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80607DDE2232306800B84D91 /* LocationPopover.swift */; }; + 80607DE4223230B700B84D91 /* Location.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 80607DE3223230B700B84D91 /* Location.storyboard */; }; + 80607DE6223230CF00B84D91 /* LocationChatItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80607DE5223230CF00B84D91 /* LocationChatItem.swift */; }; + 80607DE9223230E000B84D91 /* LocationCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 80607DE7223230E000B84D91 /* LocationCell.xib */; }; + 80607DEA223230E000B84D91 /* LocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80607DE8223230E000B84D91 /* LocationCell.swift */; }; 8062E327209E19BB0044F407 /* AuthClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8062E326209E19BB0044F407 /* AuthClient.swift */; }; 8062E32920A1CAAB0044F407 /* SubscriptionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8062E32820A1CAAB0044F407 /* SubscriptionsRequest.swift */; }; 8062E32C20A1F8100044F407 /* RoomsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8062E32B20A1F8100044F407 /* RoomsRequest.swift */; }; @@ -1508,6 +1516,14 @@ 805DEC341FFC03380033151B /* CustomEmojiManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiManager.swift; sourceTree = ""; }; 805DEC361FFC08870033151B /* CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmoji.swift; sourceTree = ""; }; 805DEC381FFE54820033151B /* CustomEmojiSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiSpec.swift; sourceTree = ""; }; + 80607DDB2232306800B84D91 /* SharedLocationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedLocationViewController.swift; sourceTree = ""; }; + 80607DDC2232306800B84D91 /* LocationShareDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationShareDelegate.swift; sourceTree = ""; }; + 80607DDD2232306800B84D91 /* LocationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationViewController.swift; sourceTree = ""; }; + 80607DDE2232306800B84D91 /* LocationPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationPopover.swift; sourceTree = ""; }; + 80607DE3223230B700B84D91 /* Location.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Location.storyboard; sourceTree = ""; }; + 80607DE5223230CF00B84D91 /* LocationChatItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationChatItem.swift; sourceTree = ""; }; + 80607DE7223230E000B84D91 /* LocationCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LocationCell.xib; sourceTree = ""; }; + 80607DE8223230E000B84D91 /* LocationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCell.swift; sourceTree = ""; }; 8062E326209E19BB0044F407 /* AuthClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthClient.swift; sourceTree = ""; }; 8062E32820A1CAAB0044F407 /* SubscriptionsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsRequest.swift; sourceTree = ""; }; 8062E32B20A1F8100044F407 /* RoomsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomsRequest.swift; sourceTree = ""; }; @@ -2516,6 +2532,7 @@ 41E2FA031D414ED400238DFD /* Subscriptions */, 414EE62721CD1F80003693D2 /* Video Conferencing */, 41865AF01FC8B1EC00A5E48F /* WebViewEmbedded */, + 80607DDA2232304B00B84D91 /* Location */, ); path = Controllers; sourceTree = ""; @@ -2532,6 +2549,7 @@ 41CD52D420BEFA3B00336892 /* New Room.storyboard */, 4102E3A91E532323004BAA82 /* Preferences.storyboard */, 41E2FA001D414EA100238DFD /* Subscriptions.storyboard */, + 80607DE3223230B700B84D91 /* Location.storyboard */, ); path = Storyboards; sourceTree = ""; @@ -3335,6 +3353,17 @@ path = UIAlertAction; sourceTree = ""; }; + 80607DDA2232304B00B84D91 /* Location */ = { + isa = PBXGroup; + children = ( + 80607DDE2232306800B84D91 /* LocationPopover.swift */, + 80607DDC2232306800B84D91 /* LocationShareDelegate.swift */, + 80607DDD2232306800B84D91 /* LocationViewController.swift */, + 80607DDB2232306800B84D91 /* SharedLocationViewController.swift */, + ); + path = Location; + sourceTree = ""; + }; 8062E32F20A3A2A30044F407 /* Subscription */ = { isa = PBXGroup; children = ( @@ -3934,6 +3963,8 @@ 4190694121D0E82E00FE2573 /* MessageVideoCallCell.swift */, 4101B02A21A2EF8C00772F7C /* HeaderCell.xib */, 4101B02C21A2EF9300772F7C /* HeaderCell.swift */, + 80607DE8223230E000B84D91 /* LocationCell.swift */, + 80607DE7223230E000B84D91 /* LocationCell.xib */, ); path = Cells; sourceTree = ""; @@ -3959,6 +3990,7 @@ 9977D85A217E999800FE5EC6 /* MessageActionsChatItem.swift */, 4190693E21D0E7F600FE2573 /* MessageVideoCallChatItem.swift */, 4101B02821A2EF6B00772F7C /* HeaderChatItem.swift */, + 80607DE5223230CF00B84D91 /* LocationChatItem.swift */, ); path = ChatItems; sourceTree = ""; @@ -4442,8 +4474,10 @@ 14F8A288202E659000175FDC /* White-60@3x.png in Resources */, 419EB5B3215E3C2200E591BF /* AudioCell.xib in Resources */, 333032A02073940800A9514D /* RCEmojiKit.strings in Resources */, + 80607DE9223230E000B84D91 /* LocationCell.xib in Resources */, 9977D857217E942200FE5EC6 /* MessageActionsCell.xib in Resources */, 994C90B62174BA5100383AFF /* VideoMessageCell.xib in Resources */, + 80607DE4223230B700B84D91 /* Location.storyboard in Resources */, 41CD52D520BEFA3B00336892 /* New Room.storyboard in Resources */, 14F8A25E202E64B200175FDC /* BnW-76@2x.png in Resources */, 411D40C920D27A5E001A1035 /* ChannelInfoDescriptionCell.xib in Resources */, @@ -4822,6 +4856,7 @@ 8013F86B1FD6B59A00EE1A4E /* Version.swift in Sources */, 41BFA0AA2146D23D008B9611 /* MessageManager.swift in Sources */, 994D1EDF205AB945007F29C8 /* UINavigationControllerExtension.swift in Sources */, + 80607DEA223230E000B84D91 /* LocationCell.swift in Sources */, 80B2D98920E5496E002F4149 /* UserDetailFieldCellModel.swift in Sources */, 4192054C1D52F4FC004EEC5F /* SubscriptionCell.swift in Sources */, 412E1F3B1DB6D55000531FDA /* ChatMessageURLView.swift in Sources */, @@ -4969,6 +5004,7 @@ 419FEAA62181F115000DF2EC /* UnreadMarkerChatItem.swift in Sources */, 41E53A171E546F5500C3FBB3 /* UINibExtensions.swift in Sources */, 8013F8711FD6B5B000EE1A4E /* InfoClient.swift in Sources */, + 80607DE02232306800B84D91 /* LocationShareDelegate.swift in Sources */, 994C90B82174BCAF00383AFF /* VideoMessageCell.swift in Sources */, 8076FDA02048519D00114F28 /* AuthManagerSocket.swift in Sources */, D32E28241DFD86C300D6019C /* AnalyticsCoordinator.swift in Sources */, @@ -4996,6 +5032,7 @@ 80CC78CF20DAE334002FBEBC /* SubscriptionsViewModel.swift in Sources */, 4190693D21D0DE0500FE2573 /* JitsiViewModel.swift in Sources */, 33E33ED620E0E59B00EF4560 /* AuthNavigationController.swift in Sources */, + 80607DE22232306800B84D91 /* LocationPopover.swift in Sources */, 9990751D21766ECE00CAB7C8 /* BaseImageMessageCell.swift in Sources */, 33F73B302073F24200F03F29 /* NotificationViewController.swift in Sources */, 99DBB8742090360600382DB2 /* MessagesListControllerSearch.swift in Sources */, @@ -5095,6 +5132,7 @@ 41FB7138215B0FD6002B5187 /* MessagesSizingManager.swift in Sources */, 995F711A20C7910800B7535F /* AuthTableViewControllerLoginServices.swift in Sources */, 80EE865321A82E6100BFEEC8 /* UIViewControllerDimming.swift in Sources */, + 80607DE6223230CF00B84D91 /* LocationChatItem.swift in Sources */, 999075252177795700CAB7C8 /* QuoteMessageCell.swift in Sources */, 809B53101FE2F17D00833DD2 /* ReactionView.swift in Sources */, D15C83861F70991F001AB155 /* APIResponse.swift in Sources */, @@ -5248,6 +5286,7 @@ 996735D221585CA70049BB63 /* BasicMessageChatItem.swift in Sources */, 800F38ED2019492D0005CB78 /* DeepLink.swift in Sources */, 998E64982161ADE200E7C45A /* TextAttachmentCell.swift in Sources */, + 80607DDF2232306800B84D91 /* SharedLocationViewController.swift in Sources */, 419EB5BD215E69FF00E591BF /* ReactionsCell.swift in Sources */, 99BE4D822152D56E001A43E2 /* MessagesViewController.swift in Sources */, 4151B4581E2D1D2E00F8AA1B /* MessageModelMapping.swift in Sources */, @@ -5255,6 +5294,7 @@ 414B3B25203E2F2C0078D3D9 /* MainSplitViewController.swift in Sources */, 99907513217622A900CAB7C8 /* FileMessageCell.swift in Sources */, 414A1FF61D46320F00093E10 /* ResponseMessage.swift in Sources */, + 80607DE12232306800B84D91 /* LocationViewController.swift in Sources */, 996735CF21582E790049BB63 /* BasicMessageCell.swift in Sources */, 4151B44E1E2CF19A00F8AA1B /* UserModelHandler.swift in Sources */, 1496A87220FA469C005C2E14 /* Dynamic.swift in Sources */, diff --git a/Rocket.Chat/Controllers/Chat/ChatSections/MessageSection.swift b/Rocket.Chat/Controllers/Chat/ChatSections/MessageSection.swift index 1d69aeed08..208a8229eb 100644 --- a/Rocket.Chat/Controllers/Chat/ChatSections/MessageSection.swift +++ b/Rocket.Chat/Controllers/Chat/ChatSections/MessageSection.swift @@ -209,15 +209,24 @@ final class MessageSection: ChatSection { } } } - + var isLocationMessage: Bool = false object.message.urls.forEach { messageURL in - cells.insert(MessageURLChatItem( - url: messageURL.url, - imageURL: messageURL.imageURL, - title: messageURL.title, - subtitle: messageURL.subtitle, - message: object.message - ).wrapped, at: 0) + if messageURL.url.range(of: "https://maps.google.com/?q=") != nil { + isLocationMessage = true + cells.insert(LocationChatItem( + url: messageURL.url, + title: object.message.text, + message: object.message + ).wrapped, at: 0) + } else { + cells.insert(MessageURLChatItem( + url: messageURL.url, + imageURL: messageURL.imageURL, + title: messageURL.title, + subtitle: messageURL.subtitle, + message: object.message + ).wrapped, at: 0) + } } if object.message.isBroadcastReplyAvailable() { @@ -242,15 +251,19 @@ final class MessageSection: ChatSection { } if !object.isSequential && shouldAppendMessageHeader { - cells.append(BasicMessageChatItem( - user: user, - message: object.message - ).wrapped) + if !isLocationMessage { + cells.append(BasicMessageChatItem( + user: user, + message: object.message + ).wrapped) + } } else if object.isSequential { - cells.append(SequentialMessageChatItem( - user: user, - message: object.message - ).wrapped) + if !isLocationMessage { + cells.append(SequentialMessageChatItem( + user: user, + message: object.message + ).wrapped) + } } if let daySeparator = object.daySeparator { diff --git a/Rocket.Chat/Controllers/Chat/MessagesViewController.swift b/Rocket.Chat/Controllers/Chat/MessagesViewController.swift index 39c24bc7b7..0f5c542fad 100644 --- a/Rocket.Chat/Controllers/Chat/MessagesViewController.swift +++ b/Rocket.Chat/Controllers/Chat/MessagesViewController.swift @@ -304,7 +304,8 @@ final class MessagesViewController: RocketChatViewController { (nib: MessageURLCell.nib, cellIdentifier: MessageURLCell.identifier), (nib: MessageActionsCell.nib, cellIdentifier: MessageActionsCell.identifier), (nib: MessageVideoCallCell.nib, cellIdentifier: MessageVideoCallCell.identifier), - (nib: HeaderCell.nib, cellIdentifier: HeaderCell.identifier) + (nib: HeaderCell.nib, cellIdentifier: HeaderCell.identifier), + (nib: LocationCell.nib, cellIdentifier: LocationCell.identifier) ] collectionViewCells.forEach { @@ -650,3 +651,23 @@ extension MessagesViewController: SocketConnectionHandler { } } + +extension MessagesViewController { + func openSharedLocationMap(for url: String, username: String) { + let storyboard = UIStoryboard(name: "Location", bundle: Bundle.main) + + let backButton = UIBarButtonItem(title: "", style: .plain, target: self, action: nil) + navigationItem.backBarButtonItem = backButton + + let coordinates = url.getCoordinates() + var isSelf: Bool = false + if let currentUser = AuthManager.currentUser(), let currentUsername = currentUser.username { + isSelf = currentUsername.isContentEqual(to: username) + } + + if let controller = storyboard.instantiateViewController(withIdentifier: "SharedLocation") as? SharedLocationViewController { + controller.setup(sharedLocation: coordinates, username: username, isSelf: isSelf) + self.navigationController?.pushViewController(controller, animated: true) + } + } +} diff --git a/Rocket.Chat/Controllers/Chat/MessagesViewControllerMessageCellProtocol.swift b/Rocket.Chat/Controllers/Chat/MessagesViewControllerMessageCellProtocol.swift index 099138bad3..1e0733af63 100644 --- a/Rocket.Chat/Controllers/Chat/MessagesViewControllerMessageCellProtocol.swift +++ b/Rocket.Chat/Controllers/Chat/MessagesViewControllerMessageCellProtocol.swift @@ -109,12 +109,20 @@ extension MessagesViewController: ChatMessageCellProtocol { } func openURL(url: URL) { - WebBrowserManager.open(url: url) + if url.absoluteString.range(of: "https://maps.google.com/?q=") != nil { + openSharedLocationMap(for: url.absoluteString, username: "") + } else { + WebBrowserManager.open(url: url) + } } - func openURLFromCell(url: String) { + func openURLFromCell(url: String, username: String) { guard let destinyURL = URL(string: url) else { return } - WebBrowserManager.open(url: destinyURL) + if url.range(of: "https://maps.google.com/?q=") != nil { + openSharedLocationMap(for: destinyURL.absoluteString, username: username) + } else { + WebBrowserManager.open(url: destinyURL) + } } func openVideoFromCell(attachment: UnmanagedAttachment) { diff --git a/Rocket.Chat/Controllers/Chat/MessagesViewControllerUploading.swift b/Rocket.Chat/Controllers/Chat/MessagesViewControllerUploading.swift index 059e4d8c48..4756287f44 100644 --- a/Rocket.Chat/Controllers/Chat/MessagesViewControllerUploading.swift +++ b/Rocket.Chat/Controllers/Chat/MessagesViewControllerUploading.swift @@ -43,6 +43,10 @@ extension MessagesViewController: MediaPicker, UIImagePickerControllerDelegate, self.openDrawing() } + addAction("chat.upload.location", image: #imageLiteral(resourceName: "Location")) { _ in + self.openLocationShare() + } + alert.addAction(UIAlertAction(title: localized("global.cancel"), style: .cancel, handler: nil)) if let presenter = alert.popoverPresentationController { @@ -291,3 +295,39 @@ extension MessagesViewController: DrawingControllerDelegate { } } + +// MARK: Share location + +extension MessagesViewController: LocationControllerDelegate { + + func openLocationShare() { + let storyboard = UIStoryboard(name: "Location", bundle: Bundle.main) + + if let controller = storyboard.instantiateInitialViewController() as? UINavigationController { + + if let locationController = controller.viewControllers.first as? LocationViewController { + locationController.delegate = self + } + + self.present(controller, animated: true, completion: nil) + } + } + + func shareLocation(with coordinates: CLLocationCoordinate2D, address: Address?) { + + let googleString = "https://maps.google.com/?q=\(coordinates.latitude),\(coordinates.longitude)" + var finalString = googleString + + if let address = address { + if address.completeAddress.isEmpty { + finalString = "\(address.placeName)\n\(googleString)" + } else { + finalString = "\(address.placeName)\n\(address.completeAddress)\n\(googleString)" + } + } + + DispatchQueue.main.async { + self.viewModel.sendTextMessage(text: finalString) + } + } +} diff --git a/Rocket.Chat/Controllers/Location/LocationPopover.swift b/Rocket.Chat/Controllers/Location/LocationPopover.swift new file mode 100644 index 0000000000..8f88fbc181 --- /dev/null +++ b/Rocket.Chat/Controllers/Location/LocationPopover.swift @@ -0,0 +1,78 @@ +// +// LocationPopover.swift +// Rocket.Chat +// +// Created by Luís Machado on 29/01/2019. +// Copyright © 2019 Rocket.Chat. All rights reserved. +// + +import UIKit + +class MyCustomButton: UIButton { + + override open var isHighlighted: Bool { + didSet { + backgroundColor = isHighlighted ? UIColor.init(displayP3Red: 240/255, green: 240/255, blue: 240/255, alpha: 0.8) : UIColor.clear + } + } +} + +class LocationPopover: UIViewController { + + @IBOutlet weak var addressLabel: UILabel! + @IBOutlet weak var acceptButton: UIButton! + @IBOutlet weak var spinner: UIActivityIndicatorView! + @IBOutlet weak var spinnerWidth: NSLayoutConstraint! + @IBOutlet weak var spinnerLeading: NSLayoutConstraint! + @IBOutlet weak var sendLocationButton: MyCustomButton! + + weak var locationViewController: LocationViewController? + var address: Address? + + override func viewDidLoad() { + super.viewDidLoad() + + acceptButton.setTitle(localized("location.send_location"), for: .normal) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + spinner.startAnimating() + setup(for: nil, stopLoad: false) + + } + + func setup(for address: Address?, stopLoad: Bool = true) { + self.address = address + addressLabel.text = address?.shortAddress ?? localized("location.loading") + + if stopLoad { + spinner.stopAnimating() + spinnerWidth.constant = 0 + spinnerLeading.constant = 0 + spinner.isHidden = true + } + + let minWidth = acceptButton.frame.size.width + 24 + + var calculatedWidth = estimateFrameForText(width: 500, text: addressLabel.text ?? "", font: UIFont.systemFont(ofSize: 13.0)).width + if !stopLoad { + calculatedWidth = 220 + } else { + calculatedWidth = (calculatedWidth < minWidth) ? minWidth : calculatedWidth + 20 + } + + self.preferredContentSize = CGSize(width: calculatedWidth + 20, height: 49) + } + + @IBAction func acceptPressed(_ sender: Any) { + locationViewController?.locationSelected(address: address) + } +} + +func estimateFrameForText(width: CGFloat, text: String, font: UIFont) -> CGRect { + let size = CGSize(width: width, height: 30) + let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) + return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: font], context: nil) +} diff --git a/Rocket.Chat/Controllers/Location/LocationShareDelegate.swift b/Rocket.Chat/Controllers/Location/LocationShareDelegate.swift new file mode 100644 index 0000000000..0034db195a --- /dev/null +++ b/Rocket.Chat/Controllers/Location/LocationShareDelegate.swift @@ -0,0 +1,14 @@ +// +// LocationShareDelegate.swift +// Rocket.Chat +// +// Created by Luís Machado on 30/01/2019. +// Copyright © 2019 Rocket.Chat. All rights reserved. +// + +import UIKit +import MapKit + +protocol LocationControllerDelegate: class { + func shareLocation(with coordinates: CLLocationCoordinate2D, address: Address?) +} diff --git a/Rocket.Chat/Controllers/Location/LocationViewController.swift b/Rocket.Chat/Controllers/Location/LocationViewController.swift new file mode 100644 index 0000000000..038ff30259 --- /dev/null +++ b/Rocket.Chat/Controllers/Location/LocationViewController.swift @@ -0,0 +1,319 @@ +// +// LocationViewController.swift +// Rocket.Chat +// +// Created by Luís Machado on 28/01/2019. +// Copyright © 2019 Rocket.Chat. All rights reserved. +// + +import UIKit +import MapKit + +class LocationViewController: UIViewController, UIGestureRecognizerDelegate { + + var selectedLocation: CLLocationCoordinate2D? + var userLocation: CLLocationCoordinate2D? + let locationManager = CLLocationManager() + var currentPopover: LocationPopover? + var showingSatellite: Bool = false + + let myAnnotation = MKPointAnnotation() + var mapRegionTimer: Timer? + var locationWasAllowed: Bool = false + + weak var delegate: LocationControllerDelegate? + + @IBOutlet weak var map: MKMapView! + @IBOutlet weak var mapOptionsButton: UIButton! + + override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { + super.willTransition(to: newCollection, with: coordinator) + + dismissPopover() + self.map.removeAnnotations(self.map.annotations) + coordinator.animate(alongsideTransition: nil) { (_) in + self.map.addAnnotation(self.myAnnotation) + self.map.selectAnnotation(self.myAnnotation, animated: true) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "arrow"), style: .plain, target: self, action: #selector(centerOnUser)) + + // For use in foreground + self.locationManager.delegate = self + self.locationManager.requestWhenInUseAuthorization() + self.map.showsUserLocation = true + self.map.showsCompass = false + self.map.delegate = self + + self.title = localized("location.title") + + if CLLocationManager.locationServicesEnabled() { + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.startUpdatingLocation() + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { //Wait 1 second to see if user location is available + if !self.locationWasAllowed { + self.selectedLocation = self.map.centerCoordinate + self.setupForLocation(location: self.map.centerCoordinate) + } + } + } + + @objc func centerOnUser() { + if let userLocation = map.userLocation.location?.coordinate { + let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) + let mapRegion = MKCoordinateRegion(center: userLocation, span: span) + map.setRegion(mapRegion, animated: true) + } else { + Alert(key: "location_disabled").present() + } + } + + @IBAction func mapOptionsPressed(_ sender: Any) { + showingSatellite = !showingSatellite + self.changeMapView(showSatellite: showingSatellite) + } + + @IBAction func cancelPressed(_ sender: Any) { + dismissController() + } + + func locationSelected(address: Address?) { + delegate?.shareLocation(with: myAnnotation.coordinate, address: address) + dismissController() + } + + func changeMapView(showSatellite: Bool) { + mapOptionsButton.setImage(showSatellite ? #imageLiteral(resourceName: "map") : #imageLiteral(resourceName: "satellite"), for: .normal) + map.mapType = showSatellite ? .hybrid : .standard + } + + override func viewWillDisappear(_ animated: Bool) { + locationManager.stopUpdatingLocation() + } + + private func dismissController() { + dismissPopover() + dismiss(animated: true, completion: nil) + } + + private func dismissPopover() { + currentPopover?.dismiss(animated: false, completion: nil) + currentPopover = nil + } +} + +extension LocationViewController: MKMapViewDelegate { + + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + dismissPopover() + mapView.view(for: myAnnotation)?.setDragState(.starting, animated: false) + setMapRegionTimer() + } + + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + mapView.view(for: myAnnotation)?.setDragState(.ending, animated: false) + mapRegionTimer?.invalidate() + map.selectAnnotation(myAnnotation, animated: true) + } + + private func setMapRegionTimer() { + mapRegionTimer?.invalidate() + mapRegionTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(mapRegionTimerFired), userInfo: nil, repeats: true) + mapRegionTimer?.fire() + } + + @objc func mapRegionTimerFired() { + myAnnotation.coordinate = self.map.centerCoordinate + } +} + +extension LocationViewController: CLLocationManagerDelegate { + + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + if status == .denied || status == .restricted || status == .notDetermined { + locationWasAllowed = false + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let location = manager.location { + + if !locationWasAllowed { + locationWasAllowed = true + selectedLocation = location.coordinate + setupForLocation(location: location.coordinate) + } + } + } + + func setupForLocation(location: CLLocationCoordinate2D) { + let viewRegion = MKCoordinateRegion(center: location, latitudinalMeters: 200, longitudinalMeters: 200) + map.setRegion(viewRegion, animated: true) + + myAnnotation.coordinate = location + + map.removeAnnotations(map.annotations) + map.addAnnotation(myAnnotation) + } + + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + + mapView.deselectAnnotation(view.annotation, animated: false) + + let storyboard = UIStoryboard(name: "Location", bundle: nil) + guard let popover = storyboard.instantiateViewController(withIdentifier: "locationPopover") as? LocationPopover else { return } + _ = popover.view + + popover.locationViewController = self + + myAnnotation.coordinate.getLocationName { (address) in + popover.setup(for: address) + } + + popover.modalPresentationStyle = .popover + popover.popoverPresentationController?.permittedArrowDirections = .any + popover.popoverPresentationController?.delegate = self + popover.popoverPresentationController?.sourceView = view + popover.popoverPresentationController?.sourceRect = view.bounds + popover.popoverPresentationController?.backgroundColor = UIColor(red: 240/255, green: 240/255, blue: 240/255, alpha: 0.85) + + // Allow these views to be accessible while popover is being displayed + var passthroughViews: [UIView] = [mapOptionsButton, map] + + if let closeButtonView = navigationController?.navigationBar { + passthroughViews.append(closeButtonView) + } + + popover.popoverPresentationController?.passthroughViews = passthroughViews + + currentPopover = popover + present(popover, animated: true, completion: nil) + } +} + +extension LocationViewController: UIPopoverPresentationControllerDelegate { + + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } +} + +struct Address { + let shortAddress: String + let placeName: String + let completeAddress: String + let headerAddress: String +} + +extension CLLocationCoordinate2D { + + func getLocationName(callback: @escaping (_ address: Address) -> Void) { + // Add below code to get address for touch coordinates. + + let geoCoder = CLGeocoder() + let location = CLLocation(latitude: latitude, longitude: longitude) + + var shortAddress: String = "" // (PIN) Street name (thoroughfare) number (sub thoroughfare), locality + var placeName: String = "" //Name + var completeAddress: String = "" //thoroughfare subthoroughfare, postal code, locality, country + var headerAddress: String = "" //thoroughfare subthoroughfare, postal code, locality, country + + geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, _) -> Void in + + // Place details + var placeMark: CLPlacemark! + + guard let placemarks = placemarks else { return } + + if placemarks.count == 0 { + return + } + + placeMark = placemarks[0] + + // Short Address + shortAddress = placeMark.name ?? "" + var shortAddressComponents: [String] = [] + var streetComponents: [String] = [] + if let thoroughfare = placeMark.thoroughfare, !thoroughfare.isEmpty { + streetComponents.append(thoroughfare) + } + if let subThoroughfare = placeMark.subThoroughfare, !subThoroughfare.isEmpty { + streetComponents.append(subThoroughfare) + } + + let streetComponentsJoined = streetComponents.joined(separator: " ") + if !streetComponentsJoined.isEmpty { + shortAddressComponents.append(streetComponentsJoined) + } + + if let locality = placeMark.locality, !locality.isEmpty { + shortAddressComponents.append(locality) + } + + let shortAddressJoined = shortAddressComponents.joined(separator: ", ") + if !shortAddressJoined.isEmpty { + shortAddress = shortAddressJoined + } + + // Place Name + placeName = placeMark.name ?? shortAddress + + // Complete Address + var addressComponentsList: [String] = [] + var addressComponentsListWOCountry: String = "" + //// Street + let streetAndNumber = streetComponents.joined(separator: " ") + if !streetAndNumber.isEmpty { + addressComponentsList.append(streetAndNumber) + } + //// Postal Code and Locality + var postalCodeAndLocalityList: [String] = [] + var postalCodeAndLocality: String = "" + + if let postalCode = placeMark.postalCode, !postalCode.isEmpty { + postalCodeAndLocalityList.append(postalCode) + } + if let locality = placeMark.locality, !locality.isEmpty { + postalCodeAndLocalityList.append(locality) + } + + postalCodeAndLocality = postalCodeAndLocalityList.joined(separator: " ") + + if !postalCodeAndLocality.isEmpty { + addressComponentsList.append(postalCodeAndLocality) + } + //// Country + addressComponentsListWOCountry = addressComponentsList.joined(separator: ", ") //Used for the header title + if let country = placeMark.country, !country.isEmpty { + addressComponentsList.append(country) + } + completeAddress = addressComponentsList.joined(separator: ", ") + + // Header Address + if placeName.isContentEqual(to: streetAndNumber) { + headerAddress = postalCodeAndLocality + } else if streetAndNumber.isEmpty { + if postalCodeAndLocality.isEmpty { + if let country = placeMark.country, !country.isEmpty { + headerAddress = "\(placeName), \(country)" + } else { + headerAddress = placeName + } + } else { + headerAddress = "\(placeName), \(postalCodeAndLocality)" + } + } else { + headerAddress = addressComponentsListWOCountry + } + + callback(Address(shortAddress: shortAddress, placeName: placeName, completeAddress: completeAddress, headerAddress: headerAddress)) + }) + } +} diff --git a/Rocket.Chat/Controllers/Location/SharedLocationViewController.swift b/Rocket.Chat/Controllers/Location/SharedLocationViewController.swift new file mode 100644 index 0000000000..4323ac4271 --- /dev/null +++ b/Rocket.Chat/Controllers/Location/SharedLocationViewController.swift @@ -0,0 +1,447 @@ +// +// SharedLocationViewController.swift +// Rocket.Chat +// +// Created by Luís Machado on 07/02/2019. +// Copyright © 2019 Rocket.Chat. All rights reserved. +// + +import UIKit +import MapKit + +enum CurrentFocus { + case sharedLocation + case userLocation + case both +} + +class SharedLocationViewController: UIViewController, UIGestureRecognizerDelegate { + + var sharedLocation: CLLocationCoordinate2D? + var usernameWhoShared: String = "" + var isSelf: Bool = true + var locationWasAllowed: Bool = false + + var currentFocus: CurrentFocus = .sharedLocation + @IBOutlet weak var changeFocusButton: UIBarButtonItem! + + var userLocation: CLLocationCoordinate2D? + let locationManager = CLLocationManager() + var currentPopover: LocationPopover? + var showingSatellite: Bool = false + + let myAnnotation = MKPointAnnotation() + var timeDistanceButton: UIButton? + var mapRegionTimer: Timer? + + weak var delegate: LocationControllerDelegate? + + @IBOutlet weak var map: MKMapView! + @IBOutlet weak var mapOptionsButton: UIButton! + + func setTitle2(title: String, subtitle: String) -> UIView { + let titleLabel = UILabel(frame: CGRect(x: 0, y: -5, width: 0, height: 0)) + + titleLabel.backgroundColor = UIColor.clear + titleLabel.textColor = UIColor.gray + titleLabel.font = UIFont.boldSystemFont(ofSize: 17) + titleLabel.text = title + titleLabel.sizeToFit() + + let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0)) + subtitleLabel.backgroundColor = UIColor.clear + subtitleLabel.textColor = UIColor.black + subtitleLabel.font = UIFont.systemFont(ofSize: 12) + subtitleLabel.text = subtitle + subtitleLabel.sizeToFit() + + let titleView = UIView(frame: CGRect(x: 0, y: 0, width: max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), height: 30)) + titleView.addSubview(titleLabel) + titleView.addSubview(subtitleLabel) + + let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width + + if widthDiff > 0 { + var frame = titleLabel.frame + frame.origin.x = widthDiff / 2 + titleLabel.frame = frame.integral + } else { + var frame = subtitleLabel.frame + frame.origin.x = abs(widthDiff) / 2 + titleLabel.frame = frame.integral + } + + return titleView + } + + func setTitle(title: String, subtitle: String) -> UIView { + let titleLabel = UILabel(frame: CGRect(x: 0, y: -2, width: 0, height: 0)) + + titleLabel.backgroundColor = .clear + titleLabel.textColor = .gray + titleLabel.font = UIFont.boldSystemFont(ofSize: 17) + titleLabel.text = title + titleLabel.sizeToFit() + + let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0)) + subtitleLabel.backgroundColor = .clear + subtitleLabel.textColor = .black + subtitleLabel.font = UIFont.systemFont(ofSize: 12) + subtitleLabel.text = subtitle + subtitleLabel.sizeToFit() + + let maxWidth = self.view.frame.width - 50 + + let titleView = UIView(frame: CGRect(x: 0, y: 0, width: min(max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), maxWidth), height: 30)) + + if titleLabel.frame.size.width > maxWidth { + titleLabel.frame = CGRect(x: titleLabel.frame.origin.x, y: titleLabel.frame.origin.y, width: maxWidth, height: titleLabel.frame.size.height) + } + + if subtitleLabel.frame.size.width > maxWidth { + subtitleLabel.frame = CGRect(x: subtitleLabel.frame.origin.x, y: subtitleLabel.frame.origin.y, width: maxWidth, height: subtitleLabel.frame.size.height) + } + + titleView.addSubview(titleLabel) + titleView.addSubview(subtitleLabel) + + let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width + + if widthDiff < 0 { + let newX = widthDiff / 2 + subtitleLabel.frame.origin.x = abs(newX) + } else { + let newX = widthDiff / 2 + titleLabel.frame.origin.x = newX + } + + return titleView + } + + override func viewDidLoad() { + super.viewDidLoad() + + // For use in foreground + self.locationManager.requestWhenInUseAuthorization() + self.map.showsUserLocation = true + self.map.showsCompass = false + self.map.delegate = self + + if CLLocationManager.locationServicesEnabled() { + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.startUpdatingLocation() + } + } + + func setup(sharedLocation: CLLocationCoordinate2D, username: String, isSelf: Bool) { + self.sharedLocation = sharedLocation + self.usernameWhoShared = username + self.isSelf = isSelf + + sharedLocation.getLocationName { (address) in + let vie = self.setTitle(title: address.placeName, subtitle: address.headerAddress) + self.navigationItem.titleView = vie + } + } + + @IBAction func sharePressed(_ sender: Any) { + guard let sharedLoc = sharedLocation else { return } + openMaps(location: sharedLoc, region: map.region, driving: false) + } + + @IBAction func changeFocusPressed(_ sender: Any) { + + if !locationWasAllowed { + currentFocus = .sharedLocation + changeMapFocus() + Alert(key: "location_disabled").present() + return + } + + switch currentFocus { + case .sharedLocation: + currentFocus = .userLocation + case .userLocation: + currentFocus = .both + case .both: + currentFocus = .sharedLocation + } + changeMapFocus() + } + + private func changeMapFocus() { + var annotationsToShow: [MKAnnotation] = [] + + for annotation in self.map.annotations { + map.deselectAnnotation(annotation, animated: true) + if annotation is MKPointAnnotation && (currentFocus == .sharedLocation || currentFocus == .both) { + annotationsToShow.append(annotation) + } else if annotation is MKUserLocation && (currentFocus == .userLocation || currentFocus == .both) { + annotationsToShow.append(annotation) + } + } + + map.showAnnotations(annotationsToShow, animated: true) + if annotationsToShow.count == 1 { + map.selectAnnotation(annotationsToShow[0], animated: true) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let location = self.sharedLocation { + setupForLocation(location: location) + } + } + + @objc func centerOnUser() { + if let userLocation = map.userLocation.location?.coordinate { + let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) + let mapRegion = MKCoordinateRegion(center: userLocation, span: span) + map.setRegion(mapRegion, animated: true) + } + } + + @IBAction func mapOptionsPressed(_ sender: Any) { + showingSatellite = !showingSatellite + self.changeMapView(showSatellite: showingSatellite) + } + + func changeMapView(showSatellite: Bool) { + mapOptionsButton.setImage(showSatellite ? #imageLiteral(resourceName: "map") : #imageLiteral(resourceName: "satellite"), for: .normal) + map.mapType = showSatellite ? .hybrid : .standard + } + + override func viewWillDisappear(_ animated: Bool) { + locationManager.stopUpdatingLocation() + dismissPopover() + } + + private func dismissPopover() { + currentPopover?.dismiss(animated: false, completion: nil) + currentPopover = nil + } +} + +extension SharedLocationViewController { + private func openMaps(location: CLLocationCoordinate2D, region: MKCoordinateRegion, driving: Bool) { + + let alert = UIAlertController(title: nil, message: driving ? localized("maps.choose_application") : nil, preferredStyle: .actionSheet) + + let apple = UIAlertAction(title: "Apple Maps", style: .default, handler: { _ in + self.openAppleMaps(location: location, region: region, driving: driving) + }) + + let google = UIAlertAction(title: "Google Maps", style: .default, handler: { _ in + self.openGoogleMaps(location: location, driving: driving) + }) + + let waze = UIAlertAction(title: "Waze", style: .default, handler: { _ in + self.openWaze(location: location, driving: driving) + }) + + alert.addAction(UIAlertAction(title: localized("global.cancel"), style: .cancel, handler: nil)) + alert.addAction(apple) + if canOpenGoogleMaps() { + alert.addAction(google) + } + + if canOpenWaze() { + alert.addAction(waze) + } + + present(alert, animated: true) + } + + private func canOpenGoogleMaps() -> Bool { + guard let baseURL = URL(string: "comgooglemaps://") else { return false } + return UIApplication.shared.canOpenURL(baseURL) + } + + private func canOpenWaze() -> Bool { + guard let baseURL = URL(string: "waze://") else { return false } + return UIApplication.shared.canOpenURL(baseURL) + } + + private func openWaze(location: CLLocationCoordinate2D, driving: Bool) { + if canOpenWaze() { + let urlString: String = "waze://?ll=\(location.latitude),\(location.longitude)&navigate=\(driving ? "yes" : "no")" + guard let mapsURL = URL(string: urlString) else { + Alert(key: "maps.open_external_error").present() + return + } + + UIApplication.shared.open(mapsURL, options: [:], completionHandler: nil) + } + } + private func openAppleMaps(location: CLLocationCoordinate2D, region: MKCoordinateRegion, driving: Bool) { + let regionSpan = region + var options: [String: Any] = [ + MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: regionSpan.center), + MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: regionSpan.span) + ] + + if driving { + options[MKLaunchOptionsDirectionsModeKey] = MKLaunchOptionsDirectionsModeDriving + } + + let placemark = MKPlacemark(coordinate: location, addressDictionary: nil) + let mapItem = MKMapItem(placemark: placemark) + mapItem.name = usernameWhoShared + mapItem.openInMaps(launchOptions: options) + } + + private func openGoogleMaps(location: CLLocationCoordinate2D, driving: Bool) { + if canOpenGoogleMaps() { + let urlString = driving ? + "comgooglemaps://?saddr=&daddr=\(location.latitude),\(location.longitude)&directionsmode=driving" : + "comgooglemaps://?center=\(location.latitude),\(location.longitude)&zoom=14&views=traffic&q=\(location.latitude),\(location.longitude)" + guard let mapsURL = URL(string: urlString) else { + Alert(key: "maps.open_external_error").present() + return + } + + UIApplication.shared.open(mapsURL, options: [:], completionHandler: nil) + } + } +} + +extension SharedLocationViewController: MKMapViewDelegate { + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + if annotation is MKUserLocation { + return nil + } + + if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "pin") { + annotationView.annotation = annotation + return annotationView + } else { + let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin") + annotationView.canShowCallout = true + + // Action Button + let smallSquare = CGSize(width: 50, height: 50) + let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare)) + button.backgroundColor = UIColor(red: 26/255, green: 105/255, blue: 243/255, alpha: 1) + button.setImage(UIImage(named: "car_map")?.withRenderingMode(.alwaysTemplate), for: .normal) + button.imageView?.tintColor = .white + button.tintColor = .white + button.setTitle(" ", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 11) + button.imageEdgeInsets = UIEdgeInsets(top: 3, left: 8, bottom: 3, right: 1) + button.imageView?.contentMode = .scaleAspectFit + timeDistanceButton = button + annotationView.leftCalloutAccessoryView = button + + return annotationView + } + } + + func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { + guard let sharedLoc = sharedLocation else { return } + + if let reuseId = view.reuseIdentifier, reuseId.isContentEqual(to: "pin") { + openMaps(location: sharedLoc, region: map.region, driving: true) + } + } +} + +extension SharedLocationViewController: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + if status == .denied || status == .restricted || status == .notDetermined { + locationWasAllowed = false + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = manager.location, let sharedLoc = sharedLocation else { return } + userLocation = location.coordinate + locationWasAllowed = true + + if !isSelf { + // Distance + let distance = calculateDistance(between: location.coordinate, pointB: sharedLoc) + let formatted = formatMetersToString(distance: distance) + myAnnotation.subtitle = formatted + } + + // ETA + let sourcePlacemark = MKPlacemark(coordinate: location.coordinate, addressDictionary: nil) + let sourceMapItem = MKMapItem(placemark: sourcePlacemark) + let destinationPlacemark = MKPlacemark(coordinate: sharedLoc, addressDictionary: nil) + let destinationMapItem = MKMapItem(placemark: destinationPlacemark) + + let request = MKDirections.Request() + request.source = sourceMapItem + request.destination = destinationMapItem + request.transportType = MKDirectionsTransportType.automobile + request.requestsAlternateRoutes = false + let directions = MKDirections(request: request) + directions.calculate { (response, _) in + if let route = response?.routes.first { + // Distances inferior to 1 min are returned as null (no need to show) + guard let formatted = self.formatSecondsToMinutesOrHours(time: route.expectedTravelTime) else { return } + + self.timeDistanceButton?.setTitle(formatted, for: .normal) + self.timeDistanceButton?.titleEdgeInsets = UIEdgeInsets(top: 30, left: -38, bottom: 0, right: 0) + self.timeDistanceButton?.imageEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 16, right: 0) + } + } + } + + func setupForLocation(location: CLLocationCoordinate2D) { + let viewRegion = MKCoordinateRegion(center: location, latitudinalMeters: 200, longitudinalMeters: 200) + map.setRegion(viewRegion, animated: true) + + myAnnotation.coordinate = location + + var title = self.usernameWhoShared + if isSelf { + title.append(" (\(localized("maps.me")))") + } + + myAnnotation.title = title + map.removeAnnotations(map.annotations) + map.addAnnotation(myAnnotation) + map.selectAnnotation(myAnnotation, animated: true) + } + + private func calculateDistance(between pointA: CLLocationCoordinate2D, pointB: CLLocationCoordinate2D) -> Double { + let loc1 = CLLocation(latitude: pointA.latitude, longitude: pointA.longitude) + let loc2 = CLLocation(latitude: pointB.latitude, longitude: pointB.longitude) + + return loc1.distance(from: loc2) + } + + private func formatMetersToString(distance: Double) -> String { + + if distance < 1000 { + return "\(Int(distance)) m" + } else { + let roundedToKms: Int = Int(distance / 1000) + return "\(roundedToKms) km" + } + } + + private func formatSecondsToMinutesOrHours(time: Double) -> String? { + + if time < 80 { + return nil + } else if time < 3600 { + let min = Int(time / 60) + return "\(min) min" + } else { + let hours = Int(time / 60 / 60) + return "\(hours) hr" + } + } +} + +extension SharedLocationViewController: UIPopoverPresentationControllerDelegate { + + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } +} diff --git a/Rocket.Chat/Info.plist b/Rocket.Chat/Info.plist index 35d84ce793..23a8e4f4a7 100644 --- a/Rocket.Chat/Info.plist +++ b/Rocket.Chat/Info.plist @@ -242,6 +242,8 @@ LSApplicationQueriesSchemes org-appextension-feature-password-management + comgooglemaps + waze googlechromes googlechrome opera-http @@ -272,6 +274,8 @@ NSPhotoLibraryAddUsageDescription NSPhotoLibraryUsageDescription NSPhotoLibraryUsageDescription + NSLocationWhenInUseUsageDescription + Location access is needed in order to determine and share your location RC_MIN_SERVER_VERSION 0.62.0 UIApplicationShortcutItems diff --git a/Rocket.Chat/Resources/Assets.xcassets/Composer/Attach/Location.imageset/Contents.json b/Rocket.Chat/Resources/Assets.xcassets/Composer/Attach/Location.imageset/Contents.json new file mode 100644 index 0000000000..de5141533f --- /dev/null +++ b/Rocket.Chat/Resources/Assets.xcassets/Composer/Attach/Location.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "location2.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Rocket.Chat/Resources/Assets.xcassets/Composer/Attach/Location.imageset/location2.pdf b/Rocket.Chat/Resources/Assets.xcassets/Composer/Attach/Location.imageset/location2.pdf new file mode 100644 index 0000000000..7b4aa56d05 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/Composer/Attach/Location.imageset/location2.pdf differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/Contents.json b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/Contents.json b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/Contents.json new file mode 100644 index 0000000000..5305751f0a --- /dev/null +++ b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "arrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "arrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow.png new file mode 100644 index 0000000000..76e4b60221 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow@2x.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow@2x.png new file mode 100644 index 0000000000..fc717323ce Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow@2x.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow@3x.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow@3x.png new file mode 100644 index 0000000000..7c99320b2c Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/arrow.imageset/arrow@3x.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/car_map.imageset/Contents.json b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/car_map.imageset/Contents.json new file mode 100644 index 0000000000..50107dd8c0 --- /dev/null +++ b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/car_map.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "car_map.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/car_map.imageset/car_map.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/car_map.imageset/car_map.png new file mode 100644 index 0000000000..5688462c91 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/car_map.imageset/car_map.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/Contents.json b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/Contents.json new file mode 100644 index 0000000000..c3dc9d099b --- /dev/null +++ b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "map.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "map@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "map@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map.png new file mode 100644 index 0000000000..a6d1731564 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map@2x.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map@2x.png new file mode 100644 index 0000000000..c295c461c3 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map@2x.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map@3x.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map@3x.png new file mode 100644 index 0000000000..fdb1d228d3 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/map.imageset/map@3x.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/Contents.json b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/Contents.json new file mode 100644 index 0000000000..9b76f514c9 --- /dev/null +++ b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "satellite.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "satellite@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "satellite@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite.png new file mode 100644 index 0000000000..4cf5ad9f94 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite@2x.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite@2x.png new file mode 100644 index 0000000000..5b24458858 Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite@2x.png differ diff --git a/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite@3x.png b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite@3x.png new file mode 100644 index 0000000000..6eaac6655d Binary files /dev/null and b/Rocket.Chat/Resources/Assets.xcassets/ShareLocation/satellite.imageset/satellite@3x.png differ diff --git a/Rocket.Chat/Resources/cs.lproj/Localizable.strings b/Rocket.Chat/Resources/cs.lproj/Localizable.strings index 0b05e78391..73be4ac222 100644 --- a/Rocket.Chat/Resources/cs.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/cs.lproj/Localizable.strings @@ -250,6 +250,17 @@ "chat.upload.choose_from_library" = "Vybrat z knihovny"; "chat.upload.import_file" = "Importovat soubor z"; "chat.upload.draw" = "Draw something"; // TODO +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Stahuji \"%@\""; diff --git a/Rocket.Chat/Resources/de.lproj/Localizable.strings b/Rocket.Chat/Resources/de.lproj/Localizable.strings index 4e01f9ffb1..86bd5b6ee6 100644 --- a/Rocket.Chat/Resources/de.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/de.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Foto & Video Mediathek"; "chat.upload.import_file" = "Importiere Datei aus"; "chat.upload.draw" = "Bild malen"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Wird heruntergeladen \"%@\""; diff --git a/Rocket.Chat/Resources/el.lproj/Localizable.strings b/Rocket.Chat/Resources/el.lproj/Localizable.strings index 421952a8cf..00de4ff520 100644 --- a/Rocket.Chat/Resources/el.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/el.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Επιλέξτε από τη Βιβλιοθήκη"; "chat.upload.import_file" = "Εισαγωγή Αρχείου Από"; "chat.upload.draw" = "Σχεδιάστε κάτι"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Κατεβάζουμε \"%@\""; diff --git a/Rocket.Chat/Resources/en.lproj/Localizable.strings b/Rocket.Chat/Resources/en.lproj/Localizable.strings index c00d96c4f2..367b3a53db 100644 --- a/Rocket.Chat/Resources/en.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/en.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Choose from library"; "chat.upload.import_file" = "Attach files"; "chat.upload.draw" = "Draw something"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; +"location.send_location" = "Send this location"; +"location.title" = "Location"; +"maps.open_external_error" = "Unable to open application"; +"maps.choose_application" = "Choose an application"; +"maps.me" = "Me"; +"location_disabled.title" = "Location Sharing Disabled"; +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; // Chat Download "chat.download.downloading_file" = "Downloading \"%@\""; diff --git a/Rocket.Chat/Resources/es.lproj/Localizable.strings b/Rocket.Chat/Resources/es.lproj/Localizable.strings index 145f7406b2..b67e8e2a60 100644 --- a/Rocket.Chat/Resources/es.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/es.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Elige de la biblioteca"; "chat.upload.import_file" = "Importar archivo desde"; "chat.upload.draw" = "Dibujar algo"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Descargando \"%@\""; diff --git a/Rocket.Chat/Resources/fr.lproj/Localizable.strings b/Rocket.Chat/Resources/fr.lproj/Localizable.strings index 721a24ba02..e67676a1b4 100644 --- a/Rocket.Chat/Resources/fr.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/fr.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Choisir dans les photos"; "chat.upload.import_file" = "Importer un fichier depuis..."; "chat.upload.draw" = "Dessiner quelque chose"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Téléchargement de \"%@\""; diff --git a/Rocket.Chat/Resources/it.lproj/Localizable.strings b/Rocket.Chat/Resources/it.lproj/Localizable.strings index 46a81a4bf0..fccb8f37a4 100644 --- a/Rocket.Chat/Resources/it.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/it.lproj/Localizable.strings @@ -250,6 +250,17 @@ "chat.upload.choose_from_library" = "Scegliere dalla libreria"; "chat.upload.import_file" = "Aggiungere dei documenti"; "chat.upload.draw" = "Disegnare qualcosa"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Scarico \"%@\""; diff --git a/Rocket.Chat/Resources/ja.lproj/Localizable.strings b/Rocket.Chat/Resources/ja.lproj/Localizable.strings index d013927885..6a04f0bd03 100644 --- a/Rocket.Chat/Resources/ja.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/ja.lproj/Localizable.strings @@ -250,6 +250,17 @@ "chat.upload.choose_from_library" = "ライブラリから選択"; "chat.upload.import_file" = "添付ファイル"; "chat.upload.draw" = "絵を描く"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "\"%@\" ダウンロード"; diff --git a/Rocket.Chat/Resources/pl.lproj/Localizable.strings b/Rocket.Chat/Resources/pl.lproj/Localizable.strings index 2891c47105..486a586b4b 100644 --- a/Rocket.Chat/Resources/pl.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/pl.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Wybierz plik z biblioteki"; "chat.upload.import_file" = "Importuj plik z"; "chat.upload.draw" = "Narysuj obrazek"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Pobieranie \"%@\""; diff --git a/Rocket.Chat/Resources/pt-BR.lproj/Localizable.strings b/Rocket.Chat/Resources/pt-BR.lproj/Localizable.strings index f596b6d19f..1faec75724 100644 --- a/Rocket.Chat/Resources/pt-BR.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/pt-BR.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Escolher da biblioteca"; "chat.upload.import_file" = "Importar arquivos"; "chat.upload.draw" = "Desenhar algo"; +"chat.upload.location" = "Partilhar localização"; + +// Location Share +"location.loading" = "Carregando..."; +"location.send_location" = "Enviar esta localização"; +"location.title" = "Localização"; +"maps.open_external_error" = "Erro ao abrir a aplicação"; +"maps.choose_application" = "Escolha uma aplicação"; +"maps.me" = "Eu"; +"location_disabled.title" = "Serviços de localização desactivados"; +"location_disabled.message" = "Os serviços de localização estão desactivados para o RocketChat. Por favor active-os nas configurações do aparelho."; // Chat Download "chat.download.downloading_file" = "Baixando \"%@\""; diff --git a/Rocket.Chat/Resources/pt-PT.lproj/Localizable.strings b/Rocket.Chat/Resources/pt-PT.lproj/Localizable.strings index 5656b60c0a..00c669b822 100644 --- a/Rocket.Chat/Resources/pt-PT.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/pt-PT.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Escolher da biblioteca"; "chat.upload.import_file" = "Enviar ficheiros"; "chat.upload.draw" = "Desenhe algo"; +"chat.upload.location" = "Partilhar localização"; + +// Location Share +"location.loading" = "Carregando..."; +"location.send_location" = "Enviar esta localização"; +"location.title" = "Localização"; +"maps.open_external_error" = "Erro ao abrir a aplicação"; +"maps.choose_application" = "Escolha uma aplicação"; +"maps.me" = "Eu"; +"location_disabled.title" = "Serviços de localização desactivados"; +"location_disabled.message" = "Os serviços de localização estão desactivados para o RocketChat. Por favor active-os nas configurações do aparelho."; // Chat Download "chat.download.downloading_file" = "Descarregando \"%@\""; diff --git a/Rocket.Chat/Resources/ru.lproj/Localizable.strings b/Rocket.Chat/Resources/ru.lproj/Localizable.strings index a14a64498f..075a70c803 100644 --- a/Rocket.Chat/Resources/ru.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/ru.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "Выбрать из библиотеки"; "chat.upload.import_file" = "Прикрепить файлы"; "chat.upload.draw" = "Нарисовать что-нибудь"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "Загрузки \"%@\""; diff --git a/Rocket.Chat/Resources/zh-Hans.lproj/Localizable.strings b/Rocket.Chat/Resources/zh-Hans.lproj/Localizable.strings index 3265b83430..134048b555 100644 --- a/Rocket.Chat/Resources/zh-Hans.lproj/Localizable.strings +++ b/Rocket.Chat/Resources/zh-Hans.lproj/Localizable.strings @@ -249,6 +249,17 @@ "chat.upload.choose_from_library" = "从图库选择"; "chat.upload.import_file" = "附件"; "chat.upload.draw" = "手绘画图"; +"chat.upload.location" = "Share location"; // TODO + +// Location Share +"location.loading" = "Loading..."; // TODO +"location.send_location" = "Send this location"; // TODO +"location.title" = "Location"; // TODO +"maps.open_external_error" = "Unable to open application"; // TODO +"maps.choose_application" = "Choose an application"; // TODO +"maps.me" = "Me"; // TODO +"location_disabled.title" = "Location Sharing Disabled"; //TODO +"location_disabled.message" = "Location sharing isn't enabled for RocketChat. Please enable it first on your phone settings."; //TODO // Chat Download "chat.download.downloading_file" = "下载中\"%@\""; diff --git a/Rocket.Chat/Storyboards/Location.storyboard b/Rocket.Chat/Storyboards/Location.storyboard new file mode 100644 index 0000000000..cf8da38e8d --- /dev/null +++ b/Rocket.Chat/Storyboards/Location.storyboard @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rocket.Chat/Views/Avatar/AvatarView.swift b/Rocket.Chat/Views/Avatar/AvatarView.swift index d9b5bdee5e..07d80ffed5 100644 --- a/Rocket.Chat/Views/Avatar/AvatarView.swift +++ b/Rocket.Chat/Views/Avatar/AvatarView.swift @@ -49,6 +49,11 @@ final class AvatarView: UIView { } } + override func layoutSubviews() { + super.layoutSubviews() + self.layer.cornerRadius = self.frame.size.width / 2 + } + func updateAvatar() { if let emoji = emoji { let emojiCharacter = Emojione.transform(string: emoji) diff --git a/Rocket.Chat/Views/Cells/Chat/ChatMessageURLView.swift b/Rocket.Chat/Views/Cells/Chat/ChatMessageURLView.swift index 1bea66a382..1ec1611adb 100644 --- a/Rocket.Chat/Views/Cells/Chat/ChatMessageURLView.swift +++ b/Rocket.Chat/Views/Cells/Chat/ChatMessageURLView.swift @@ -9,7 +9,7 @@ import UIKit protocol ChatMessageURLViewProtocol: class { - func openURLFromCell(url: String) + func openURLFromCell(url: String, username: String) } final class ChatMessageURLView: UIView { diff --git a/Rocket.Chat/Views/Chat/New Chat/Cells/LocationCell.swift b/Rocket.Chat/Views/Chat/New Chat/Cells/LocationCell.swift new file mode 100644 index 0000000000..8e4c345cdd --- /dev/null +++ b/Rocket.Chat/Views/Chat/New Chat/Cells/LocationCell.swift @@ -0,0 +1,116 @@ +// +// LocationCell.swift +// Rocket.Chat +// +// Created by Luís Machado on 06/02/2019. +// Copyright © 2019 Rocket.Chat. All rights reserved. +// + +import UIKit +import RocketChatViewController +import MapKit + +final class LocationCell: BaseMessageCell, SizingCell { + static let identifier = String(describing: LocationCell.self) + + static let sizingCell: UICollectionViewCell & ChatCell = { + guard let cell = LocationCell.instantiateFromNib() else { + return LocationCell() + } + + return cell + }() + + @IBOutlet weak var containerView: UIView! { + didSet { + containerView.layer.borderWidth = 1 + containerView.layer.cornerRadius = 4 + } + } + + @IBOutlet weak var title: UILabel! + @IBOutlet weak var subtitle: UILabel! + @IBOutlet weak var thumbnail: UIImageView! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + @IBOutlet weak var host: UILabel! + + @IBOutlet weak var thumbnailHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var containerWidthConstraint: NSLayoutConstraint! + @IBOutlet weak var containerLeadingConstraint: NSLayoutConstraint! + @IBOutlet weak var containerTrailingConstraint: NSLayoutConstraint! + var containerWidth: CGFloat { + return + messageWidth - + containerLeadingConstraint.constant - + containerTrailingConstraint.constant - + layoutMargins.left - + layoutMargins.right + } + + var thumbnailHeightInitialConstant: CGFloat = 0 + + override func prepareForReuse() { + super.prepareForReuse() + thumbnail.image = nil + host.text = "" + subtitle.text = "" + } + + override func awakeFromNib() { + super.awakeFromNib() + + thumbnailHeightInitialConstant = thumbnailHeightConstraint.constant + + let gesture = UITapGestureRecognizer(target: self, action: #selector(didTapContainerView)) + gesture.delegate = self + containerView.addGestureRecognizer(gesture) + + insertGesturesIfNeeded(with: nil) + } + + override func configure(completeRendering: Bool) { + guard let viewModel = viewModel?.base as? LocationChatItem else { + return + } + + containerWidthConstraint.constant = containerWidth + + // Generate map + if viewModel.coordinates.latitude != 0 && viewModel.coordinates.longitude != 0 { + thumbnailHeightConstraint.constant = thumbnailHeightInitialConstant + activityIndicator.startAnimating() + viewModel.generateImage {[weak self] (image) in + self?.thumbnail.image = image + self?.activityIndicator.stopAnimating() + } + } else { + thumbnailHeightConstraint.constant = 0 + } + + host.text = viewModel.shortAddress + subtitle.text = viewModel.longAdress + } + + @objc func didTapContainerView() { + guard + let viewModel = viewModel, + let locationChatItemItem = viewModel.base as? LocationChatItem + else { + return + } + + delegate?.openURLFromCell(url: locationChatItemItem.url, username: locationChatItemItem.message?.user?.username ?? "") + } +} + +extension LocationCell { + override func applyTheme() { + super.applyTheme() + + let theme = self.theme ?? .light + containerView.backgroundColor = theme.chatComponentBackground + host.textColor = theme.auxiliaryText + subtitle.textColor = theme.controlText + containerView.layer.borderColor = theme.borderColor.cgColor + } +} diff --git a/Rocket.Chat/Views/Chat/New Chat/Cells/LocationCell.xib b/Rocket.Chat/Views/Chat/New Chat/Cells/LocationCell.xib new file mode 100644 index 0000000000..4314793b3b --- /dev/null +++ b/Rocket.Chat/Views/Chat/New Chat/Cells/LocationCell.xib @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rocket.Chat/Views/Chat/New Chat/Cells/MessageURLCell.swift b/Rocket.Chat/Views/Chat/New Chat/Cells/MessageURLCell.swift index 3ff76c3f46..95aaa16d1a 100644 --- a/Rocket.Chat/Views/Chat/New Chat/Cells/MessageURLCell.swift +++ b/Rocket.Chat/Views/Chat/New Chat/Cells/MessageURLCell.swift @@ -93,7 +93,7 @@ final class MessageURLCell: BaseMessageCell, SizingCell { return } - delegate?.openURLFromCell(url: messageURLChatItem.url) + delegate?.openURLFromCell(url: messageURLChatItem.url, username: "") } } diff --git a/Rocket.Chat/Views/Chat/New Chat/ChatItems/LocationChatItem.swift b/Rocket.Chat/Views/Chat/New Chat/ChatItems/LocationChatItem.swift new file mode 100644 index 0000000000..51a4e1bb77 --- /dev/null +++ b/Rocket.Chat/Views/Chat/New Chat/ChatItems/LocationChatItem.swift @@ -0,0 +1,125 @@ +// +// LocationChatItem.swift +// Rocket.Chat +// +// Created by Luís Machado on 06/02/2019. +// Copyright © 2019 Rocket.Chat. All rights reserved. +// + +import Foundation +import DifferenceKit +import RocketChatViewController +import MapKit + +extension String { + func getCoordinates() -> CLLocationCoordinate2D { + var coordinates: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0) + let coordinatesText = self.replacingOccurrences(of: "https://maps.google.com/?q=", with: "") + + let splitCoordinates = coordinatesText.split(separator: ",") + if splitCoordinates.count == 2, let latitude = Double(splitCoordinates[0]), let longitude = Double(splitCoordinates[1]) { + coordinates = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + } + + return coordinates + } +} + +final class LocationChatItem: BaseMessageChatItem, ChatItem, Differentiable { + var relatedReuseIdentifier: String { + return LocationCell.identifier + } + + var url: String + var coordinates: CLLocationCoordinate2D + var shortAddress: String + var longAdress: String + + init(url: String, title: String, message: UnmanagedMessage?) { + + self.url = url + self.coordinates = url.getCoordinates() + self.shortAddress = "" + self.longAdress = "" + let completeAddress = title.replacingOccurrences(of: "\n\(url)", with: "") + + let splitAddresses = completeAddress.split(separator: "\n") + + if splitAddresses.count <= 1 { + let addressParts = completeAddress.split(separator: ",") + if addressParts.count > 0 { + self.shortAddress = String(addressParts[0]) + } + + self.longAdress = completeAddress + } else if splitAddresses.count == 2 { + self.shortAddress = String(splitAddresses[0]) + self.longAdress = String(splitAddresses[1]) + } + + super.init(user: nil, message: message) + } + + func generateImage(completion: @escaping (UIImage?) -> Void) { + let mapSnapshotOptions = MKMapSnapshotter.Options() + + let span = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.006) + let region = MKCoordinateRegion(center: coordinates, span: span) + + // Set the region of the map that is rendered. + mapSnapshotOptions.region = region + + // Set the scale of the image. We'll just use the scale of the current device, which is 2x scale on Retina screens. + mapSnapshotOptions.scale = UIScreen.main.scale + + // Set the size of the image output. + mapSnapshotOptions.size = CGSize(width: 300, height: 300) + + // Show buildings and Points of Interest on the snapshot + mapSnapshotOptions.showsBuildings = true + mapSnapshotOptions.showsPointsOfInterest = true + + let snapShotter = MKMapSnapshotter(options: mapSnapshotOptions) + let rect = CGRect(x: 0, y: 0, width: 300, height: 300) + + snapShotter.start { (snapshot, error) in + guard let snapshot = snapshot, error == nil else { + completion(nil) + return + } + + UIGraphicsBeginImageContextWithOptions(mapSnapshotOptions.size, true, 0) + snapshot.image.draw(at: .zero) + + let pinView = MKPinAnnotationView(annotation: nil, reuseIdentifier: nil) + let pinImage = pinView.image + + var point = snapshot.point(for: self.coordinates) + + if rect.contains(point) { + let pinCenterOffset = pinView.centerOffset + point.x -= pinView.bounds.size.width / 2 + point.y -= pinView.bounds.size.height / 2 + point.x += pinCenterOffset.x + point.y += pinCenterOffset.y + pinImage?.draw(at: point) + } + + let image = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + + completion(image) + } + } + + var differenceIdentifier: String { + return url + } + + func isContentEqual(to source: LocationChatItem) -> Bool { + return shortAddress == source.shortAddress && + longAdress == source.longAdress && + url == source.url + } +}