Skip to content

Commit ba80a94

Browse files
Add Static Location and Live Location Support (#3531)
* Add static and live location payloads * Update the demo app to use static location attachment * Add new `ChannelController.sendStaticLocation()` to instantly send a location message to a channel * Change the Demo App to send the current location instead of dummy ones * Add location background mode to Demo App * Add staticLocation to the attachments register * Create a CurrentUserLocationProvider to make it easier to fetch the current user location * Fix not being able to part live location payload * Update DemoApp to support live location attachments * Add support for partial message update in MessageUpdater * Expose the Throttler (Revert this, and use it internally) * Add `shareLiveLocation()` and `stopLiveLocation()` to `ChannelController` * Refactor logic to fetch current active locations + Improve API * Add extra data to location attachment payloads * Add `LocationAttachmentInfo` to be used as argument * Add `text` to partial message update endpoint * Add `ChatMessageController.updateMessage()` to support partially updating a message * Change location live updates APIs * Improve Demo App Location Provider * Improve API by observing active live locations messages * Fixed starting monitoring location whenever an active location was updated * Make the API even easier and add additional delegate methods to know when the user should start and stop location sharing * Move the Throttler to LLC, just like the Debouncer * Move the Throttling to the current user controller to automatically protect against abusive updates * Some minor cleanups * Simplify currentUserControllerDidStartSharingLiveLocation API * Improve location attachment view * Add live location map when tapping the live location attachmen * Animate the user location tracking * Optimal animation * Optmizate animation smoothness and server spamm * Add CoreData concurrency flag to StreamDevelopers scheme * Fix crash when creating the MessageController from a background thread * Fix snapshot chaching * Fix map detail view controller not showing initial position * Fix avatar view in map view * Add pulse animation when live sharing * Refactor LocationDetailViewController to only use the message controller * Refactor code structure of the map detail view controller * Add bottom sheet to stop location sharing * Add stopLiveLocationSharing() to Message Controller * Fix stop sharing button not working * FIx bottom sheet logic * Add static pin in detail view * Fix sharing location for other users active location messages * Finish logic for location snapshot view when static vs live * Add live location status view in the snapshot view * Minor cleanup * Fix copyright * Fix loading indicator snapshot view * Remove support of mixed attachments to locations * Add MessageEndpoints test coverage * Add test coverage to message updater * Add test coverage to Message Repository * Add test coverage to message attachments extensions * Add test coverage to parsing attachments * Add test coverage to MessageDTO * Add message updater mock * ActiveLiveLocationAlreadyExists init should not be public * Add test coverage to Message Controller * Add test coverage to Channel Controller * Fix concurrency issues when stopping and updating the live location at the same time * Fix Message Controller Tests * Change updateMessage -> partialUpdateMessage * Add unset support for partial update message * Update CHANGELOG.md * Fix tests, not compiling because of unset * Fix test_updatePartialMessage_makesCorrectAPICall * Make `ChatMessageController.updateLiveLocation()` internal * Update CHANGELOG.md * Fix reloading the snapshot when not necesasry * Fix avatar view showing for a split second in the snapshot view for static attachments * Change location attachment to have dynamic height depending on message list size * Extract avatar size in snapshot view * Fix minor typo * Add fixed width to map snapshot and simplify caching logic * Use a banner view instead of a sheet in the map detail view * Present the map instead of pushing when on iPad * Add more documentation on how "Tracking" behaviour works * Enable locations by default in the demo app * Fix quote message for live location * Fix location attachments should not be editable * Do not show location attachment picker when inside thread * Fix preview message for location attachments * Fix detail map show Stop Sharing button for another user * Disable locations feature by default * Add experimental flag * Update CHANGELOG.md * Revert "Disable locations feature by default" This reverts commit 0765272. * Revert "Add experimental flag" This reverts commit d06eda0. * Revert "Update CHANGELOG.md" This reverts commit 37bd1d1. * Fix missing stuff from merge conflicts * Update CHANGELOG.md * Rename LocationAttachmentInfo to LocationInfo * Add LocationDTO * Update Atlantis * Refactor to new SharedLocation object * Demo App new Static Location integration * Fix Merge Conflicts Errors * Add new location endpoints * Implement new stop live location sharing * Remove stopLiveLocationSharing from ChannelController * Implement optimistic stop live location sharing * Add `CurrentUserController.loadLiveLocations()` * Add new updateLiveLocation endpoint instead of using partial update message * Migrate existing unit tests to the new implementation * Fix not possible to open location detail view * Remove unnecessary location optimistic update * Auto-centering feature * Fix sharing location of failed messages * Fix stopping a live location not triggering didStopLiveLocationSharing delegate * Update CHANGELOG.md * Fix parsing active current active location messages * Remove unused message updater in ChannelController * Fix docs typo * Add action sheet to select live location in the demo app * Make atlantis version static * Fix unit tests compilation * Fix removeAllData test * Remove `didChangeActiveLiveLocationMessages` since it is not needed for now * Improve docs of current user controller * Revert "Remove `didChangeActiveLiveLocationMessages` since it is not needed for now" This reverts commit 37f4290. # Conflicts: # Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift * Make sure live location is changed to not active when reaches the end * Forward to the current user delegate event if there was an error updating live location * Fix potential memory leak * Use message updater directly in the current user controller * Add option to provide location info from createNewMessage * Only call didStartSharingLiveLocation or didStopSharingLiveLocation if are from the current device * Handle multi device logic when updating live location sharing * Revert Multi-Device logic for now * Fix compilation tests * Add message updater tests * Add test coverage to loadAllActiveLiveLocations * Add test coverage to use device Id when creating new live location message * Update CHANGELOG.md * Fix flaky test_stopLiveLocationSharing_apiFailure_revertsOptimisticUpdate * Render endAt text in the location attachment images * Fix not able to share 1min location * Fix auto center button showing in static locations * Add `channel.config.sharedLocationsEnabled` * Add `canShareLocation` capability * Add `ChatChannel.activeLiveLocations` * Rename LocationDTO -> SharedLocationDTO * Fix Channel.json typo * Add `ActiveLiveLocationsEndTimeTracker` to track when the endAt is reached * Fix concurrency issue in ActiveLiveLocationsEndTimeTracker * Add missing properties to shared location * Optimistically stop active live location when starting a new one * Expose `CurrentUserController.activeLiveLocationMessages` * Remove outdated comment * Add test coverage to ActiveLiveLocationsEndTimeTracker * Fix the ActiveLiveLocationsEndTimeTracker not triggering an UI Update * Try to make test less flaky * Add missing docs to `CurrentUserController.loadActiveLocationsMessages()` --------- Co-authored-by: Alexey Alter-Pesotskiy <[email protected]>
1 parent ad8e9fc commit ba80a94

File tree

84 files changed

+4022
-320
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+4022
-320
lines changed

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
## StreamChat
7+
### ✅ Added
8+
- Add `ChatMessageController.partialUpdateMessage()` [#3531](https://github.com/GetStream/stream-chat-swift/pull/3531)
9+
- Add Location Sharing Support [#3531](https://github.com/GetStream/stream-chat-swift/pull/3531)
10+
- Add `ChatMessage.sharedLocation`
11+
- Add `ChatMessageController.stopLiveLocationSharing()`
12+
- Add `ChatChannelController`:
13+
- `sendStaticLocation()`
14+
- `startLiveLocationSharing()`
15+
- Add `CurrentChatUserController`:
16+
- `loadActiveLiveLocationMessages()`
17+
- `updateLiveLocation()`
18+
- Add `CurrentChatUserControllerDelegate`:
19+
- `didStartSharingLiveLocation()`
20+
- `didStopSharingLiveLocation()`
21+
- `didChangeActiveLiveLocationMessages()`
22+
- `didFailToUpdateLiveLocation()`
23+
724

825
# [4.80.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.80.0)
926
_June 17, 2025_

DemoApp/Info.plist

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>NSBonjourServices</key>
6-
<array>
7-
<string>_Proxyman._tcp</string>
8-
</array>
9-
<key>NSLocalNetworkUsageDescription</key>
10-
<string>Atlantis would use Bonjour Service to discover Proxyman app from your local network.</string>
11-
<key>NSCameraUsageDescription</key>
12-
<string>We need access to your camera for sending photo attachments.</string>
13-
<key>NSMicrophoneUsageDescription</key>
14-
<string>We need access to your microphone for taking a video.</string>
155
<key>CFBundleDevelopmentRegion</key>
166
<string>$(DEVELOPMENT_LANGUAGE)</string>
177
<key>CFBundleExecutable</key>
@@ -30,8 +20,26 @@
3020
<string>$(CURRENT_PROJECT_VERSION)</string>
3121
<key>ITSAppUsesNonExemptEncryption</key>
3222
<false/>
23+
<key>LSApplicationCategoryType</key>
24+
<string></string>
3325
<key>LSRequiresIPhoneOS</key>
3426
<true/>
27+
<key>NSBonjourServices</key>
28+
<array>
29+
<string>_Proxyman._tcp</string>
30+
</array>
31+
<key>NSCameraUsageDescription</key>
32+
<string>We need access to your camera for sending photo attachments.</string>
33+
<key>NSLocalNetworkUsageDescription</key>
34+
<string>Atlantis would use Bonjour Service to discover Proxyman app from your local network.</string>
35+
<key>NSLocationAlwaysUsageDescription</key>
36+
<string>We need access to your location to share it in the chat.</string>
37+
<key>NSLocationWhenInUseUsageDescription</key>
38+
<string>We need access to your location to share it in the chat.</string>
39+
<key>NSMicrophoneUsageDescription</key>
40+
<string>We need access to your microphone for taking a video.</string>
41+
<key>PushNotification-Configuration</key>
42+
<string>APN-Configuration</string>
3543
<key>UIApplicationSceneManifest</key>
3644
<dict>
3745
<key>UIApplicationSupportsMultipleScenes</key>
@@ -51,6 +59,10 @@
5159
</dict>
5260
<key>UIApplicationSupportsIndirectInputEvents</key>
5361
<true/>
62+
<key>UIBackgroundModes</key>
63+
<array>
64+
<string>location</string>
65+
</array>
5466
<key>UILaunchStoryboardName</key>
5567
<string>LaunchScreen</string>
5668
<key>UIRequiredDeviceCapabilities</key>
@@ -70,7 +82,5 @@
7082
<string>UIInterfaceOrientationLandscapeLeft</string>
7183
<string>UIInterfaceOrientationLandscapeRight</string>
7284
</array>
73-
<key>PushNotification-Configuration</key>
74-
<string>APN-Configuration</string>
7585
</dict>
7686
</plist>

DemoApp/LocationProvider.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import CoreLocation
6+
import Foundation
7+
8+
enum LocationPermissionError: Error {
9+
case permissionDenied
10+
case permissionRestricted
11+
}
12+
13+
class LocationProvider: NSObject {
14+
private let locationManager: CLLocationManager
15+
private var onCurrentLocationFetch: ((Result<CLLocation, Error>) -> Void)?
16+
17+
var didUpdateLocation: ((CLLocation) -> Void)?
18+
var lastLocation: CLLocation?
19+
var onError: ((Error) -> Void)?
20+
21+
private init(locationManager: CLLocationManager = CLLocationManager()) {
22+
self.locationManager = locationManager
23+
super.init()
24+
}
25+
26+
static let shared = LocationProvider()
27+
28+
var isMonitoringLocation: Bool {
29+
locationManager.delegate != nil
30+
}
31+
32+
func startMonitoringLocation() {
33+
locationManager.allowsBackgroundLocationUpdates = true
34+
locationManager.delegate = self
35+
requestPermission { [weak self] error in
36+
guard let error else { return }
37+
self?.onError?(error)
38+
}
39+
}
40+
41+
func stopMonitoringLocation() {
42+
locationManager.allowsBackgroundLocationUpdates = false
43+
locationManager.stopUpdatingLocation()
44+
locationManager.delegate = nil
45+
}
46+
47+
func getCurrentLocation(completion: @escaping (Result<CLLocation, Error>) -> Void) {
48+
onCurrentLocationFetch = completion
49+
if let lastLocation = lastLocation {
50+
onCurrentLocationFetch?(.success(lastLocation))
51+
onCurrentLocationFetch = nil
52+
} else {
53+
requestPermission { [weak self] error in
54+
guard let error else { return }
55+
self?.onCurrentLocationFetch?(.failure(error))
56+
self?.onCurrentLocationFetch = nil
57+
}
58+
}
59+
}
60+
61+
func requestPermission(completion: @escaping (Error?) -> Void) {
62+
locationManager.delegate = self
63+
switch locationManager.authorizationStatus {
64+
case .notDetermined:
65+
locationManager.requestWhenInUseAuthorization()
66+
completion(nil)
67+
case .authorizedWhenInUse, .authorizedAlways:
68+
locationManager.startUpdatingLocation()
69+
completion(nil)
70+
case .denied:
71+
completion(LocationPermissionError.permissionDenied)
72+
case .restricted:
73+
completion(LocationPermissionError.permissionRestricted)
74+
@unknown default:
75+
break
76+
}
77+
}
78+
}
79+
80+
extension LocationProvider: CLLocationManagerDelegate {
81+
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
82+
let status = manager.authorizationStatus
83+
if status == .authorizedWhenInUse || status == .authorizedAlways {
84+
manager.startUpdatingLocation()
85+
}
86+
}
87+
88+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
89+
guard let location = locations.first else { return }
90+
didUpdateLocation?(location)
91+
lastLocation = location
92+
onCurrentLocationFetch?(.success(location))
93+
onCurrentLocationFetch = nil
94+
}
95+
96+
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
97+
onError?(error)
98+
}
99+
}

DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class AppConfig {
5151
isHardDeleteEnabled: false,
5252
isAtlantisEnabled: false,
5353
isMessageDebuggerEnabled: false,
54-
isLocationAttachmentsEnabled: false,
54+
isLocationAttachmentsEnabled: true,
5555
tokenRefreshDetails: nil,
5656
shouldShowConnectionBanner: false,
5757
isPremiumMemberFeatureEnabled: false,
@@ -62,8 +62,6 @@ class AppConfig {
6262
if StreamRuntimeCheck.isStreamInternalConfiguration {
6363
demoAppConfig.isAtlantisEnabled = true
6464
demoAppConfig.isMessageDebuggerEnabled = true
65-
demoAppConfig.isLocationAttachmentsEnabled = true
66-
demoAppConfig.isLocationAttachmentsEnabled = true
6765
demoAppConfig.isHardDeleteEnabled = true
6866
demoAppConfig.shouldShowConnectionBanner = true
6967
demoAppConfig.isPremiumMemberFeatureEnabled = true

DemoApp/Screens/DemoAppTabBarController.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
// Copyright © 2025 Stream.io Inc. All rights reserved.
33
//
44

5+
import Combine
56
import StreamChat
67
import StreamChatUI
78
import UIKit
89

910
class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDelegate, MessageReminderListControllerDelegate {
11+
private var locationProvider = LocationProvider.shared
12+
1013
let channelListVC: UIViewController
1114
let threadListVC: UIViewController
1215
let draftListVC: UIViewController
@@ -59,6 +62,7 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
5962
super.viewDidLoad()
6063

6164
currentUserController.delegate = self
65+
currentUserController.loadActiveLiveLocationMessages()
6266
unreadCount = currentUserController.unreadCount
6367

6468
// Update reminders badge if the feature is enabled.
@@ -78,6 +82,13 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
7882
threadListVC.tabBarItem.image = UIImage(systemName: "text.bubble")
7983
threadListVC.tabBarItem.badgeColor = .red
8084

85+
locationProvider.didUpdateLocation = { [weak self] location in
86+
let newLocation = LocationInfo(
87+
latitude: location.coordinate.latitude,
88+
longitude: location.coordinate.longitude
89+
)
90+
self?.currentUserController.updateLiveLocation(newLocation)
91+
}
8192
draftListVC.tabBarItem.title = "Drafts"
8293
draftListVC.tabBarItem.image = UIImage(systemName: "bubble.and.pencil")
8394

@@ -99,6 +110,37 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
99110
UIApplication.shared.applicationIconBadgeNumber = totalUnreadBadge
100111
}
101112

113+
func currentUserControllerDidStartSharingLiveLocation(
114+
_ controller: CurrentChatUserController
115+
) {
116+
debugPrint("[Location] Started sharing live location.")
117+
locationProvider.startMonitoringLocation()
118+
}
119+
120+
func currentUserControllerDidStopSharingLiveLocation(_ controller: CurrentChatUserController) {
121+
debugPrint("[Location] Stopped sharing live location.")
122+
locationProvider.stopMonitoringLocation()
123+
}
124+
125+
func currentUserController(
126+
_ controller: CurrentChatUserController,
127+
didChangeActiveLiveLocationMessages messages: [ChatMessage]
128+
) {
129+
guard !messages.isEmpty else {
130+
return
131+
}
132+
133+
let locations: [String] = messages.compactMap {
134+
guard let location = $0.sharedLocation else {
135+
return nil
136+
}
137+
138+
return "(lat:\(location.latitude), lon:\(location.longitude), endAt: \(location.endAt?.description ?? "nil"))"
139+
}
140+
141+
debugPrint("[Location] Updated live locations to the server: \(locations)")
142+
}
143+
102144
func controller(
103145
_ controller: MessageReminderListController,
104146
didChangeReminders changes: [ListChange<MessageReminder>]

DemoApp/StreamChat/Components/CustomAttachments/DemoAttachmentViewCatalog.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import StreamChatUI
88
class DemoAttachmentViewCatalog: AttachmentViewCatalog {
99
override class func attachmentViewInjectorClassFor(message: ChatMessage, components: Components) -> AttachmentViewInjector.Type? {
1010
let hasMultipleAttachmentTypes = message.attachmentCounts.keys.count > 1
11-
let hasLocationAttachment = message.attachmentCounts.keys.contains(.location)
11+
let hasLocationAttachment = message.sharedLocation != nil
1212
if AppConfig.shared.demoAppConfig.isLocationAttachmentsEnabled && hasLocationAttachment {
1313
if hasMultipleAttachmentTypes {
1414
return MixedAttachmentViewInjector.self

0 commit comments

Comments
 (0)