Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Static Location and Live Location Support #3531

Closed
wants to merge 88 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
340f10c
Add static and live location payloads
nuno-vieira Dec 11, 2024
92d15ac
Update the demo app to use static location attachment
nuno-vieira Dec 11, 2024
962ffb4
Add new `ChannelController.sendStaticLocation()` to instantly send a …
nuno-vieira Dec 11, 2024
cb365fc
Change the Demo App to send the current location instead of dummy ones
nuno-vieira Dec 11, 2024
8d3b662
Add location background mode to Demo App
nuno-vieira Dec 11, 2024
b35f54e
Add staticLocation to the attachments register
nuno-vieira Dec 12, 2024
afc3f68
Create a CurrentUserLocationProvider to make it easier to fetch the c…
nuno-vieira Dec 12, 2024
736c08e
Fix not being able to part live location payload
nuno-vieira Dec 12, 2024
154e394
Update DemoApp to support live location attachments
nuno-vieira Dec 13, 2024
3b4d322
Add support for partial message update in MessageUpdater
nuno-vieira Dec 13, 2024
b9e1f2c
Expose the Throttler (Revert this, and use it internally)
nuno-vieira Dec 13, 2024
ec2f59a
Add `shareLiveLocation()` and `stopLiveLocation()` to `ChannelControl…
nuno-vieira Jan 3, 2025
0a1e120
Refactor logic to fetch current active locations + Improve API
nuno-vieira Dec 16, 2024
61a6a70
Add extra data to location attachment payloads
nuno-vieira Dec 17, 2024
64881a1
Add `LocationAttachmentInfo` to be used as argument
nuno-vieira Dec 17, 2024
6f57eb9
Add `text` to partial message update endpoint
nuno-vieira Dec 17, 2024
caf76ab
Add `ChatMessageController.updateMessage()` to support partially upda…
nuno-vieira Dec 17, 2024
5d09df8
Change location live updates APIs
nuno-vieira Dec 17, 2024
667739f
Improve Demo App Location Provider
nuno-vieira Dec 18, 2024
713c5e8
Improve API by observing active live locations messages
nuno-vieira Dec 18, 2024
54d623f
Fixed starting monitoring location whenever an active location was up…
nuno-vieira Dec 18, 2024
495b358
Make the API even easier and add additional delegate methods to know …
nuno-vieira Dec 18, 2024
b09bf38
Move the Throttler to LLC, just like the Debouncer
nuno-vieira Dec 18, 2024
6468d0d
Move the Throttling to the current user controller to automatically p…
nuno-vieira Dec 18, 2024
effff35
Some minor cleanups
nuno-vieira Dec 18, 2024
1e684db
Simplify currentUserControllerDidStartSharingLiveLocation API
nuno-vieira Dec 19, 2024
81ad0f4
Improve location attachment view
nuno-vieira Dec 20, 2024
9ded30e
Add live location map when tapping the live location attachmen
nuno-vieira Dec 23, 2024
7df6c3e
Animate the user location tracking
nuno-vieira Dec 23, 2024
b91fa09
Optimal animation
nuno-vieira Dec 23, 2024
e50a63b
Optmizate animation smoothness and server spamm
nuno-vieira Dec 23, 2024
1e9ef44
Add CoreData concurrency flag to StreamDevelopers scheme
nuno-vieira Dec 26, 2024
98afdba
Fix crash when creating the MessageController from a background thread
nuno-vieira Dec 26, 2024
7a0bd66
Fix snapshot chaching
nuno-vieira Dec 26, 2024
ecbb7e2
Fix map detail view controller not showing initial position
nuno-vieira Dec 26, 2024
ec15a92
Fix avatar view in map view
nuno-vieira Jan 2, 2025
5e3881d
Add pulse animation when live sharing
nuno-vieira Jan 2, 2025
64bcf7e
Refactor LocationDetailViewController to only use the message controller
nuno-vieira Jan 2, 2025
f3460be
Refactor code structure of the map detail view controller
nuno-vieira Jan 2, 2025
0af646e
Add bottom sheet to stop location sharing
nuno-vieira Jan 3, 2025
544b506
Add stopLiveLocationSharing() to Message Controller
nuno-vieira Jan 3, 2025
0777d9f
Fix stop sharing button not working
nuno-vieira Jan 3, 2025
b2dc128
FIx bottom sheet logic
nuno-vieira Jan 3, 2025
45ad2f1
Add static pin in detail view
nuno-vieira Jan 3, 2025
a3a252e
Fix sharing location for other users active location messages
nuno-vieira Jan 3, 2025
6cf17a3
Finish logic for location snapshot view when static vs live
nuno-vieira Jan 3, 2025
26a5e72
Add live location status view in the snapshot view
nuno-vieira Jan 3, 2025
e9be9de
Minor cleanup
nuno-vieira Jan 3, 2025
37713b8
Fix copyright
nuno-vieira Jan 3, 2025
ef4a214
Fix loading indicator snapshot view
nuno-vieira Jan 3, 2025
722cbfc
Remove support of mixed attachments to locations
nuno-vieira Jan 3, 2025
a347d00
Add MessageEndpoints test coverage
nuno-vieira Jan 6, 2025
c58a3c3
Add test coverage to message updater
nuno-vieira Jan 6, 2025
776a991
Add test coverage to Message Repository
nuno-vieira Jan 6, 2025
d3dfd2e
Add test coverage to message attachments extensions
nuno-vieira Jan 6, 2025
047f962
Add test coverage to parsing attachments
nuno-vieira Jan 6, 2025
ea5802e
Add test coverage to MessageDTO
nuno-vieira Jan 6, 2025
4712649
Add message updater mock
nuno-vieira Jan 6, 2025
0a46f19
ActiveLiveLocationAlreadyExists init should not be public
nuno-vieira Jan 6, 2025
6be1283
Add test coverage to Message Controller
nuno-vieira Jan 6, 2025
5044362
Add test coverage to Channel Controller
nuno-vieira Jan 6, 2025
2957975
Fix concurrency issues when stopping and updating the live location a…
nuno-vieira Jan 7, 2025
a73406b
Fix Message Controller Tests
nuno-vieira Jan 7, 2025
e203dfa
Change updateMessage -> partialUpdateMessage
nuno-vieira Jan 7, 2025
5c30dbe
Add unset support for partial update message
nuno-vieira Jan 7, 2025
1d7ab64
Update CHANGELOG.md
nuno-vieira Jan 7, 2025
251fa84
Fix tests, not compiling because of unset
nuno-vieira Jan 7, 2025
a211f5d
Fix test_updatePartialMessage_makesCorrectAPICall
nuno-vieira Jan 7, 2025
5b64e9a
Make `ChatMessageController.updateLiveLocation()` internal
nuno-vieira Jan 7, 2025
95d359f
Update CHANGELOG.md
nuno-vieira Jan 7, 2025
a8bf8de
Fix reloading the snapshot when not necesasry
nuno-vieira Jan 8, 2025
9c351a8
Fix avatar view showing for a split second in the snapshot view for s…
nuno-vieira Jan 8, 2025
b035ed6
Change location attachment to have dynamic height depending on messag…
nuno-vieira Jan 8, 2025
2ff83a0
Extract avatar size in snapshot view
nuno-vieira Jan 8, 2025
3c0cf34
Fix minor typo
nuno-vieira Jan 8, 2025
601fded
Add fixed width to map snapshot and simplify caching logic
nuno-vieira Jan 8, 2025
e8b6587
Use a banner view instead of a sheet in the map detail view
nuno-vieira Jan 8, 2025
61882ef
Present the map instead of pushing when on iPad
nuno-vieira Jan 8, 2025
f7dbf60
Add more documentation on how "Tracking" behaviour works
nuno-vieira Jan 8, 2025
7b60602
Enable locations by default in the demo app
nuno-vieira Jan 8, 2025
cb135d0
Fix quote message for live location
nuno-vieira Jan 9, 2025
f3d7025
Fix location attachments should not be editable
nuno-vieira Jan 9, 2025
5952dd6
Do not show location attachment picker when inside thread
nuno-vieira Jan 9, 2025
cd18446
Fix preview message for location attachments
nuno-vieira Jan 9, 2025
77baae2
Fix detail map show Stop Sharing button for another user
nuno-vieira Jan 9, 2025
0765272
Disable locations feature by default
nuno-vieira Jan 9, 2025
d06eda0
Add experimental flag
nuno-vieira Jan 9, 2025
37bd1d1
Update CHANGELOG.md
nuno-vieira Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### ✅ Added
- Add `FilterKey.disabled` and `ChatChannel.isDisabled` [#3546](https://github.com/GetStream/stream-chat-swift/pull/3546)
- Add `ImageAttachmentPayload.file` for setting `file_size` and `mime_type` for image attachments [#3548](https://github.com/GetStream/stream-chat-swift/pull/3548)
- Add `ChatMessageController.partialUpdateMessage()` [#3531](https://github.com/GetStream/stream-chat-swift/pull/3531)
### 🐞 Fixed
- Remove the main thread requirement from the `DataStore` [#3541](https://github.com/GetStream/stream-chat-swift/pull/3541)
- Refresh quoted message preview when the quoted message is deleted [#3553](https://github.com/GetStream/stream-chat-swift/pull/3553)
Expand Down
34 changes: 22 additions & 12 deletions DemoApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBonjourServices</key>
<array>
<string>_Proxyman._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Atlantis would use Bonjour Service to discover Proxyman app from your local network.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for sending photo attachments.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for taking a video.</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
Expand All @@ -30,8 +20,26 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBonjourServices</key>
<array>
<string>_Proxyman._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for sending photo attachments.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Atlantis would use Bonjour Service to discover Proxyman app from your local network.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>We need access to your location to share it in the chat.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need access to your location to share it in the chat.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for taking a video.</string>
<key>PushNotification-Configuration</key>
<string>APN-Configuration</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
Expand All @@ -51,6 +59,10 @@
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
Expand All @@ -70,7 +82,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>PushNotification-Configuration</key>
<string>APN-Configuration</string>
</dict>
</plist>
99 changes: 99 additions & 0 deletions DemoApp/LocationProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import CoreLocation
import Foundation

enum LocationPermissionError: Error {
case permissionDenied
case permissionRestricted
}

class LocationProvider: NSObject {
private let locationManager: CLLocationManager
private var onCurrentLocationFetch: ((Result<CLLocation, Error>) -> Void)?

var didUpdateLocation: ((CLLocation) -> Void)?
var lastLocation: CLLocation?
var onError: ((Error) -> Void)?

private init(locationManager: CLLocationManager = CLLocationManager()) {
self.locationManager = locationManager
super.init()
}

static let shared = LocationProvider()

var isMonitoringLocation: Bool {
locationManager.delegate != nil
}

func startMonitoringLocation() {
locationManager.allowsBackgroundLocationUpdates = true
locationManager.delegate = self
requestPermission { [weak self] error in
guard let error else { return }
self?.onError?(error)
}
}

func stopMonitoringLocation() {
locationManager.allowsBackgroundLocationUpdates = false
locationManager.stopUpdatingLocation()
locationManager.delegate = nil
}

func getCurrentLocation(completion: @escaping (Result<CLLocation, Error>) -> Void) {
onCurrentLocationFetch = completion
if let lastLocation = lastLocation {
onCurrentLocationFetch?(.success(lastLocation))
onCurrentLocationFetch = nil
} else {
requestPermission { [weak self] error in
guard let error else { return }
self?.onCurrentLocationFetch?(.failure(error))
self?.onCurrentLocationFetch = nil
}
}
}

func requestPermission(completion: @escaping (Error?) -> Void) {
locationManager.delegate = self
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
completion(nil)
case .authorizedWhenInUse, .authorizedAlways:
locationManager.startUpdatingLocation()
completion(nil)
case .denied:
completion(LocationPermissionError.permissionDenied)
case .restricted:
completion(LocationPermissionError.permissionRestricted)
@unknown default:
break
}
}
}

extension LocationProvider: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
if status == .authorizedWhenInUse || status == .authorizedAlways {
manager.startUpdatingLocation()
}
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
didUpdateLocation?(location)
lastLocation = location
onCurrentLocationFetch?(.success(location))
onCurrentLocationFetch = nil
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
onError?(error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ class AppConfig {
if StreamRuntimeCheck.isStreamInternalConfiguration {
demoAppConfig.isAtlantisEnabled = true
demoAppConfig.isMessageDebuggerEnabled = true
demoAppConfig.isLocationAttachmentsEnabled = true
demoAppConfig.isLocationAttachmentsEnabled = true
demoAppConfig.isHardDeleteEnabled = true
demoAppConfig.isLocationAttachmentsEnabled = true
demoAppConfig.shouldShowConnectionBanner = true
demoAppConfig.isPremiumMemberFeatureEnabled = true
StreamRuntimeCheck.assertionsEnabled = true
Expand Down
43 changes: 43 additions & 0 deletions DemoApp/Screens/DemoAppTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import Combine
@_spi(ExperimentalLocation)
import StreamChat
import StreamChatUI
import UIKit

class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDelegate {
private var locationProvider = LocationProvider.shared

let channelListVC: UIViewController
let threadListVC: UIViewController
let currentUserController: CurrentChatUserController
Expand Down Expand Up @@ -61,6 +65,14 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
threadListVC.tabBarItem.badgeColor = .red

viewControllers = [channelListVC, threadListVC]

locationProvider.didUpdateLocation = { [weak self] location in
let newLocation = LocationAttachmentInfo(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
self?.currentUserController.updateLiveLocation(newLocation)
}
}

func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUserUnreadCount: UnreadCount) {
Expand All @@ -69,4 +81,35 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
let totalUnreadBadge = unreadCount.channels + unreadCount.threads
UIApplication.shared.applicationIconBadgeNumber = totalUnreadBadge
}

func currentUserControllerDidStartSharingLiveLocation(
_ controller: CurrentChatUserController
) {
debugPrint("[Location] Started sharing live location.")
locationProvider.startMonitoringLocation()
}

func currentUserControllerDidStopSharingLiveLocation(_ controller: CurrentChatUserController) {
debugPrint("[Location] Stopped sharing live location.")
locationProvider.stopMonitoringLocation()
}

func currentUserController(
_ controller: CurrentChatUserController,
didChangeActiveLiveLocationMessages messages: [ChatMessage]
) {
guard !messages.isEmpty else {
return
}

let locations: [String] = messages.compactMap {
guard let locationAttachment = $0.liveLocationAttachments.first else {
return nil
}

return "(\(locationAttachment.latitude), \(locationAttachment.longitude))"
}

debugPrint("[Location] Updated live locations to the server: \(locations)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import StreamChatUI
class DemoAttachmentViewCatalog: AttachmentViewCatalog {
override class func attachmentViewInjectorClassFor(message: ChatMessage, components: Components) -> AttachmentViewInjector.Type? {
let hasMultipleAttachmentTypes = message.attachmentCounts.keys.count > 1
let hasLocationAttachment = message.attachmentCounts.keys.contains(.location)
let hasStaticLocationAttachment = message.attachmentCounts.keys.contains(.staticLocation)
let hasLiveLocationAttachment = message.attachmentCounts.keys.contains(.liveLocation)
let hasLocationAttachment = hasStaticLocationAttachment || hasLiveLocationAttachment
if AppConfig.shared.demoAppConfig.isLocationAttachmentsEnabled && hasLocationAttachment {
if hasMultipleAttachmentTypes {
return MixedAttachmentViewInjector.self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,87 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import CoreLocation
@_spi(ExperimentalLocation)
import StreamChat
import StreamChatUI
import UIKit

class DemoComposerVC: ComposerVC {
/// For demo purposes the locations are hard-coded.
var dummyLocations: [(latitude: Double, longitude: Double)] = [
(38.708442, -9.136822), // Lisbon, Portugal
(37.983810, 23.727539), // Athens, Greece
(53.149118, -6.079341), // Greystones, Ireland
(41.11722, 20.80194), // Ohrid, Macedonia
(51.5074, -0.1278), // London, United Kingdom
(52.5200, 13.4050), // Berlin, Germany
(40.4168, -3.7038), // Madrid, Spain
(50.4501, 30.5234), // Kyiv, Ukraine
(41.9028, 12.4964), // Rome, Italy
(48.8566, 2.3522), // Paris, France
(44.4268, 26.1025), // Bucharest, Romania
(48.2082, 16.3738), // Vienna, Austria
(47.4979, 19.0402) // Budapest, Hungary
]
private var locationProvider = LocationProvider.shared

override var attachmentsPickerActions: [UIAlertAction] {
var actions = super.attachmentsPickerActions

let alreadyHasLocation = content.attachments.map(\.type).contains(.location)
if AppConfig.shared.demoAppConfig.isLocationAttachmentsEnabled && !alreadyHasLocation {
if AppConfig.shared.demoAppConfig.isLocationAttachmentsEnabled && content.isInsideThread == false {
let sendLocationAction = UIAlertAction(
title: "Location",
title: "Send Current Location",
style: .default,
handler: { [weak self] _ in self?.sendLocation() }
handler: { [weak self] _ in
self?.sendInstantStaticLocation()
}
)
actions.append(sendLocationAction)

let sendLiveLocationAction = UIAlertAction(
title: "Share Live Location",
style: .default,
handler: { [weak self] _ in
self?.sendInstantLiveLocation()
}
)
actions.append(sendLiveLocationAction)
}

return actions
}

func sendLocation() {
guard let location = dummyLocations.randomElement() else { return }
let locationAttachmentPayload = LocationAttachmentPayload(
coordinate: .init(latitude: location.latitude, longitude: location.longitude)
)
func sendInstantStaticLocation() {
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved
getCurrentLocationInfo { [weak self] location in
guard let location = location else { return }
self?.channelController?.sendStaticLocation(location)
}
}

func sendInstantLiveLocation() {
getCurrentLocationInfo { [weak self] location in
guard let location = location else { return }
self?.channelController?.startLiveLocationSharing(location)
}
}

content.attachments.append(AnyAttachmentPayload(payload: locationAttachmentPayload))
private func getCurrentLocationInfo(completion: @escaping (LocationAttachmentInfo?) -> Void) {
locationProvider.getCurrentLocation { [weak self] result in
switch result {
case .success(let location):
let location = LocationAttachmentInfo(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
completion(location)
case .failure(let error):
if error is LocationPermissionError {
self?.showLocationPermissionAlert()
}
completion(nil)
}
}
}

// In case you would want to send the location directly, without composer preview:
// channelController?.createNewMessage(text: "", attachments: [.init(
// payload: locationAttachmentPayload
// )])
private func showLocationPermissionAlert() {
let alert = UIAlertController(
title: "Location Access Required",
message: "Please enable location access in Settings to share your location.",
preferredStyle: .alert
)

alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

present(alert, animated: true)
}
}
Loading
Loading