Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/sdk-performance-metrics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
with:
ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }}

- uses: actions/setup-python@v5
with:
python-version: 3.12 # gsutil requires Python version 3.8-3.12

- uses: actions/[email protected]
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
with:
Expand Down
27 changes: 13 additions & 14 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,24 @@ jobs:
- uses: actions/[email protected]

- uses: actions/github-script@v6
id: get_pr_data
id: get_pr_number
with:
script: |
return (
await github.rest.repos.listPullRequestsAssociatedWithCommit({
commit_sha: context.sha,
owner: context.repo.owner,
repo: context.repo.repo,
})
).data[0];

- uses: ./.github/actions/bootstrap
env:
INSTALL_SONAR: true
SKIP_MINT_BOOTSTRAP: true
const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({
commit_sha: context.sha,
owner: context.repo.owner,
repo: context.repo.repo,
});
return prs.data[0]?.number || '';

- name: Run Sonar analysis
run: |
ARTIFACT_NAME="test-coverage-${{ fromJson(steps.get_pr_data.outputs.result).number }}"
if [[ -z "${{ steps.get_pr_number.outputs.result }}" ]]; then
echo "No PR found. Skipping Sonar analysis."
exit 0
fi

ARTIFACT_NAME="test-coverage-${{ steps.get_pr_number.outputs.result }}"
ARTIFACT=$(gh api repos/${{ github.repository }}/actions/artifacts | jq -r ".artifacts | map(select(.name==\"$ARTIFACT_NAME\")) | first")
if [[ "$ARTIFACT" == null || "$ARTIFACT" == "" ]]; then
echo "Artifact not found. Skipping Sonar analysis."
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### 🔄 Changed

# [4.81.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.81.0)
_July 03, 2025_

## StreamChat
### ✅ Added
- Add support for Push v3 notification types [#3708](https://github.com/GetStream/stream-chat-swift/pull/3708)
- Add `ChatMessageController.partialUpdateMessage()` [#3531](https://github.com/GetStream/stream-chat-swift/pull/3531)
- Add Location Sharing Support [#3531](https://github.com/GetStream/stream-chat-swift/pull/3531)
- Add `ChatMessage.sharedLocation`
- Add `ChatMessageController.stopLiveLocationSharing()`
- Add `ChatChannelController`:
- `sendStaticLocation()`
- `startLiveLocationSharing()`
- Add `CurrentChatUserController`:
- `loadActiveLiveLocationMessages()`
- `updateLiveLocation()`
- Add `CurrentChatUserControllerDelegate`:
- `didStartSharingLiveLocation()`
- `didStopSharingLiveLocation()`
- `didChangeActiveLiveLocationMessages()`
- `didFailToUpdateLiveLocation()`

## StreamChatUI
### 🐞 Fixed
- Fix message actions view with flag action when user has no capability [#3705](https://github.com/GetStream/stream-chat-swift/pull/3705)
- Fix reaction picker in `ChatMessagePopupVC` below the notch in rare scenarios [#3707](https://github.com/GetStream/stream-chat-swift/pull/3707)

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

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>
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ struct DemoAppConfig {
var isAtlantisEnabled: Bool
/// A Boolean value to define if an additional message debugger action will be added.
var isMessageDebuggerEnabled: Bool
/// A Boolean value to define if custom location attachments are enabled.
var isLocationAttachmentsEnabled: Bool
/// Set this value to define if we should mimic token refresh scenarios.
var tokenRefreshDetails: TokenRefreshDetails?
/// A Boolean value that determines if a connection banner UI should be shown.
Expand Down Expand Up @@ -51,7 +49,6 @@ class AppConfig {
isHardDeleteEnabled: false,
isAtlantisEnabled: false,
isMessageDebuggerEnabled: false,
isLocationAttachmentsEnabled: false,
tokenRefreshDetails: nil,
shouldShowConnectionBanner: false,
isPremiumMemberFeatureEnabled: false,
Expand All @@ -62,8 +59,6 @@ class AppConfig {
if StreamRuntimeCheck.isStreamInternalConfiguration {
demoAppConfig.isAtlantisEnabled = true
demoAppConfig.isMessageDebuggerEnabled = true
demoAppConfig.isLocationAttachmentsEnabled = true
demoAppConfig.isLocationAttachmentsEnabled = true
demoAppConfig.isHardDeleteEnabled = true
demoAppConfig.shouldShowConnectionBanner = true
demoAppConfig.isPremiumMemberFeatureEnabled = true
Expand Down Expand Up @@ -177,7 +172,6 @@ class AppConfigViewController: UITableViewController {
case isHardDeleteEnabled
case isAtlantisEnabled
case isMessageDebuggerEnabled
case isLocationAttachmentsEnabled
case tokenRefreshDetails
case shouldShowConnectionBanner
case isPremiumMemberFeatureEnabled
Expand Down Expand Up @@ -328,10 +322,6 @@ class AppConfigViewController: UITableViewController {
cell.accessoryView = makeSwitchButton(demoAppConfig.isMessageDebuggerEnabled) { [weak self] newValue in
self?.demoAppConfig.isMessageDebuggerEnabled = newValue
}
case .isLocationAttachmentsEnabled:
cell.accessoryView = makeSwitchButton(demoAppConfig.isLocationAttachmentsEnabled) { [weak self] newValue in
self?.demoAppConfig.isLocationAttachmentsEnabled = newValue
}
case .tokenRefreshDetails:
if let tokenRefreshDuration = demoAppConfig.tokenRefreshDetails?.expirationDuration {
cell.detailTextLabel?.text = "Duration before expired: \(tokenRefreshDuration)s"
Expand Down
42 changes: 42 additions & 0 deletions DemoApp/Screens/DemoAppTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import Combine
import StreamChat
import StreamChatUI
import UIKit

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

let channelListVC: UIViewController
let threadListVC: UIViewController
let draftListVC: UIViewController
Expand Down Expand Up @@ -59,6 +62,7 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
super.viewDidLoad()

currentUserController.delegate = self
currentUserController.loadActiveLiveLocationMessages()
unreadCount = currentUserController.unreadCount

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

locationProvider.didUpdateLocation = { [weak self] location in
let newLocation = LocationInfo(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
self?.currentUserController.updateLiveLocation(newLocation)
}
draftListVC.tabBarItem.title = "Drafts"
draftListVC.tabBarItem.image = UIImage(systemName: "bubble.and.pencil")

Expand All @@ -99,6 +110,37 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
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 location = $0.sharedLocation else {
return nil
}

return "(lat:\(location.latitude), lon:\(location.longitude), endAt: \(location.endAt?.description ?? "nil"))"
}

debugPrint("[Location] Updated live locations to the server: \(locations)")
}

func controller(
_ controller: MessageReminderListController,
didChangeReminders changes: [ListChange<MessageReminder>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,10 @@ 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)
if AppConfig.shared.demoAppConfig.isLocationAttachmentsEnabled && hasLocationAttachment {
if hasMultipleAttachmentTypes {
return MixedAttachmentViewInjector.self
}
let hasLocationAttachment = message.sharedLocation != nil
if hasLocationAttachment {
return LocationAttachmentViewInjector.self
}

return super.attachmentViewInjectorClassFor(message: message, components: components)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,37 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import CoreLocation
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
]

override var attachmentsPickerActions: [UIAlertAction] {
var actions = super.attachmentsPickerActions
let alreadyHasLocation = content.attachments.map(\.type).contains(.location)
if AppConfig.shared.demoAppConfig.isLocationAttachmentsEnabled && !alreadyHasLocation {
let sendLocationAction = UIAlertAction(

let isLocationEnabled = channelController?.channel?.config.sharedLocationsEnabled == true
if isLocationEnabled && content.isInsideThread == false {
let locationAction = UIAlertAction(
title: "Location",
style: .default,
handler: { [weak self] _ in self?.sendLocation() }
handler: { [weak self] _ in
self?.presentLocationSelection()
}
)
actions.append(sendLocationAction)
actions.append(locationAction)
}

return actions
}

func sendLocation() {
guard let location = dummyLocations.randomElement() else { return }
let locationAttachmentPayload = LocationAttachmentPayload(
coordinate: .init(latitude: location.latitude, longitude: location.longitude)
)

content.attachments.append(AnyAttachmentPayload(payload: locationAttachmentPayload))

// In case you would want to send the location directly, without composer preview:
// channelController?.createNewMessage(text: "", attachments: [.init(
// payload: locationAttachmentPayload
// )])
func presentLocationSelection() {
guard let channelController = channelController else { return }

let locationSelectionVC = LocationSelectionViewController(channelController: channelController)
let navigationController = UINavigationController(rootViewController: locationSelectionVC)

present(navigationController, animated: true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,21 @@ import StreamChatUI
import UIKit

class DemoQuotedChatMessageView: QuotedChatMessageView {
override func setAttachmentPreview(for message: ChatMessage) {
let locationAttachments = message.attachments(payloadType: LocationAttachmentPayload.self)
if let locationPayload = locationAttachments.first?.payload {
attachmentPreviewView.contentMode = .scaleAspectFit
attachmentPreviewView.image = UIImage(
systemName: "mappin.circle.fill",
withConfiguration: UIImage.SymbolConfiguration(font: .boldSystemFont(ofSize: 12))
)
attachmentPreviewView.tintColor = .systemRed
textView.text = """
Location:
(\(locationPayload.coordinate.latitude),\(locationPayload.coordinate.longitude))
"""
return
}
override func updateContent() {
super.updateContent()

super.setAttachmentPreview(for: message)
if let sharedLocation = content?.message.sharedLocation {
if sharedLocation.isLive {
attachmentPreviewView.contentMode = .scaleAspectFit
attachmentPreviewView.image = UIImage(systemName: "location.fill")
attachmentPreviewView.tintColor = .systemBlue
textView.text = "Live Location"
} else {
attachmentPreviewView.contentMode = .scaleAspectFit
attachmentPreviewView.image = UIImage(systemName: "mappin.circle.fill")
attachmentPreviewView.tintColor = .systemRed
textView.text = "Location"
}
}
}
}
Loading
Loading