Skip to content

Commit 8546774

Browse files
authored
Improve Push Support (#1412)
1 parent 47b9c88 commit 8546774

20 files changed

+1215
-117
lines changed

DemoApp/AppDelegate.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright © 2021 Stream.io Inc. All rights reserved.
33
//
44

5+
import StreamChat
56
import UIKit
67

78
@main
@@ -14,6 +15,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
1415
true
1516
}
1617

18+
func application(
19+
_ application: UIApplication,
20+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
21+
) {
22+
guard let currentUserId = ChatClient.shared.currentUserId else {
23+
log.warning("cannot add the device without connecting as user first, did you call connectUser")
24+
return
25+
}
26+
27+
ChatClient.shared.currentUserController().addDevice(token: deviceToken) { error in
28+
if let error = error {
29+
log.error("adding a device failed with an error \(error)")
30+
return
31+
}
32+
let defaults = UserDefaults.standard
33+
defaults.set(currentUserId, forKey: currentUserIdRegisteredForPush)
34+
}
35+
}
36+
1737
// MARK: UISceneSession Lifecycle
1838

1939
func application(

DemoApp/ChatSample.entitlements

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>aps-environment</key>
6+
<string>development</string>
7+
<key>com.apple.security.application-groups</key>
8+
<array>
9+
<string>group.io.getstream.iOS.ChatDemoApp</string>
10+
</array>
11+
</dict>
12+
</plist>

DemoApp/DemoAppCoordinator.swift

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import StreamChat
66
import StreamChatUI
77
import UIKit
88

9-
final class DemoAppCoordinator {
9+
extension ChatClient {
10+
static var shared: ChatClient!
11+
}
12+
13+
final class DemoAppCoordinator: NSObject, UNUserNotificationCenterDelegate {
1014
var connectionController: ChatConnectionController?
1115
let navigationController: UINavigationController
1216
let connectionDelegate: BannerShowingConnectionDelegate
13-
17+
1418
init(navigationController: UINavigationController) {
1519
// Since log is first touched in `BannerShowingConnectionDelegate`,
1620
// we need to set log level here
@@ -20,20 +24,70 @@ final class DemoAppCoordinator {
2024
connectionDelegate = BannerShowingConnectionDelegate(
2125
showUnder: navigationController.navigationBar
2226
)
27+
super.init()
28+
2329
injectActions()
2430
}
2531

26-
func presentChat(userCredentials: UserCredentials) {
32+
func userNotificationCenter(
33+
_ center: UNUserNotificationCenter,
34+
didReceive response: UNNotificationResponse,
35+
withCompletionHandler completionHandler: @escaping () -> Void
36+
) {
37+
defer {
38+
completionHandler()
39+
}
40+
41+
guard let notificationInfo = try? ChatPushNotificationInfo(content: response.notification.request.content) else {
42+
return
43+
}
44+
45+
guard let cid = notificationInfo.cid else {
46+
return
47+
}
48+
49+
guard case UNNotificationDefaultActionIdentifier = response.actionIdentifier else {
50+
return
51+
}
52+
53+
if let userId = UserDefaults.standard.string(forKey: currentUserIdRegisteredForPush),
54+
let userCredentials = UserCredentials.builtInUsersByID(id: userId) {
55+
presentChat(userCredentials: userCredentials, channelID: cid)
56+
}
57+
}
58+
59+
func setupRemoteNotifications() {
60+
UNUserNotificationCenter
61+
.current()
62+
.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
63+
if granted {
64+
DispatchQueue.main.async {
65+
UIApplication.shared.registerForRemoteNotifications()
66+
}
67+
}
68+
}
69+
}
70+
71+
func presentChat(userCredentials: UserCredentials, channelID: ChannelId? = nil) {
2772
// Create a token
2873
let token = try! Token(rawValue: userCredentials.token)
2974

3075
// Create client
31-
let config = ChatClientConfig(apiKey: .init(userCredentials.apiKey))
32-
let client = ChatClient(config: config)
33-
client.connectUser(
34-
userInfo: .init(id: userCredentials.id, extraData: [ChatUser.birthLandFieldName: .string(userCredentials.birthLand)]),
76+
var config = ChatClientConfig(apiKey: .init(apiKeyString))
77+
config.isLocalStorageEnabled = true
78+
config.applicationGroupIdentifier = applicationGroupIdentifier
79+
80+
ChatClient.shared = ChatClient(config: config)
81+
ChatClient.shared.connectUser(
82+
userInfo: .init(id: userCredentials.id, name: userCredentials.name, imageURL: userCredentials.avatarURL),
3583
token: token
36-
)
84+
) { error in
85+
if let error = error {
86+
log.error("connecting the user failed \(error)")
87+
return
88+
}
89+
self.setupRemoteNotifications()
90+
}
3791

3892
// Config
3993
Components.default.channelListRouter = DemoChatChannelListRouter.self
@@ -44,20 +98,28 @@ final class DemoAppCoordinator {
4498
}
4599

46100
// Channels with the current user
47-
let controller = client.channelListController(query: .init(filter: .containMembers(userIds: [userCredentials.id])))
101+
let controller = ChatClient.shared
102+
.channelListController(query: .init(filter: .containMembers(userIds: [userCredentials.id])))
48103
let chatList = DemoChannelListVC()
49104
chatList.controller = controller
50105

51-
connectionController = client.connectionController()
106+
connectionController = ChatClient.shared.connectionController()
52107
connectionController?.delegate = connectionDelegate
53108

54109
navigationController.viewControllers = [chatList]
55110
navigationController.isNavigationBarHidden = false
56111

57-
let window = navigationController.view.window!
112+
// Init the channel VC and navigate there directly
113+
if let cid = channelID {
114+
let channelVC = ChatChannelVC()
115+
channelVC.channelController = ChatClient.shared.channelController(for: cid)
116+
navigationController.viewControllers.append(channelVC)
117+
}
58118

119+
let window = navigationController.view.window!
59120
UIView.transition(with: window, duration: 0.3, options: .transitionFlipFromRight, animations: {
60121
window.rootViewController = self.navigationController
122+
61123
})
62124
}
63125

DemoApp/DemoUsers.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44

55
import Foundation
66

7-
let apiKeyString = "8br4watad788"
7+
public let apiKeyString = "8br4watad788"
8+
public let applicationGroupIdentifier = "group.io.getstream.iOS.ChatDemoApp"
9+
public let currentUserIdRegisteredForPush = "currentUserIdRegisteredForPush"
810

9-
struct UserCredentials {
11+
public struct UserCredentials {
1012
let id: String
1113
let name: String
1214
let avatarURL: URL
1315
let token: String
14-
let apiKey: String
1516
let birthLand: String
1617
}
1718

18-
extension UserCredentials {
19+
public extension UserCredentials {
20+
static func builtInUsersByID(id: String) -> UserCredentials? {
21+
builtInUsers.filter { $0.id == id }.first
22+
}
23+
1924
static let builtInUsers: [UserCredentials] = [
2025
(
2126
"luke_skywalker",
@@ -131,6 +136,6 @@ extension UserCredentials {
131136
)
132137

133138
].map {
134-
UserCredentials(id: $0.0, name: $0.1, avatarURL: URL(string: $0.2)!, token: $0.3, apiKey: apiKeyString, birthLand: $0.4)
139+
UserCredentials(id: $0.0, name: $0.1, avatarURL: URL(string: $0.2)!, token: $0.3, birthLand: $0.4)
135140
}
136141
}

DemoApp/SceneDelegate.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
2222
).instantiateInitialViewController() as? UINavigationController else { return }
2323
window.rootViewController = navigationController
2424
coordinator = DemoAppCoordinator(navigationController: navigationController)
25+
26+
UNUserNotificationCenter.current().delegate = coordinator
27+
28+
if let notificationResponse = connectionOptions.notificationResponse {
29+
print("debugging: \(notificationResponse)")
30+
print("debugging: \(notificationResponse.actionIdentifier)")
31+
print("debugging: \(notificationResponse.notification)")
32+
}
33+
2534
self.window = window
2635
window.makeKeyAndVisible()
2736
scene.windows.forEach { $0.tintColor = .streamBlue }

DemoAppPush/DemoAppPush.entitlements

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.application-groups</key>
6+
<array>
7+
<string>group.io.getstream.iOS.ChatDemoApp</string>
8+
</array>
9+
</dict>
10+
</plist>

DemoAppPush/Info.plist

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>DemoAppPush</string>
9+
<key>CFBundleExecutable</key>
10+
<string>$(EXECUTABLE_NAME)</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>$(PRODUCT_NAME)</string>
17+
<key>CFBundlePackageType</key>
18+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>1.0</string>
21+
<key>CFBundleVersion</key>
22+
<string>1</string>
23+
<key>NSExtension</key>
24+
<dict>
25+
<key>NSExtensionPointIdentifier</key>
26+
<string>com.apple.usernotifications.service</string>
27+
<key>NSExtensionPrincipalClass</key>
28+
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
29+
</dict>
30+
</dict>
31+
</plist>

DemoAppPush/NotificationService.swift

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import StreamChat
2+
import UserNotifications
3+
4+
class NotificationService: UNNotificationServiceExtension {
5+
var contentHandler: ((UNNotificationContent) -> Void)?
6+
var request: UNNotificationRequest?
7+
8+
func addAttachments(
9+
url: URL,
10+
content: UNMutableNotificationContent,
11+
identifier: String = "image",
12+
completion: @escaping (UNMutableNotificationContent) -> Void
13+
) {
14+
let task = URLSession.shared.downloadTask(with: url) { (downloadedUrl, _, _) in
15+
defer {
16+
completion(content)
17+
}
18+
19+
guard let downloadedUrl = downloadedUrl else {
20+
return
21+
}
22+
23+
guard let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
24+
return
25+
}
26+
27+
let localURL = URL(fileURLWithPath: path).appendingPathComponent(url.lastPathComponent)
28+
29+
do {
30+
try FileManager.default.moveItem(at: downloadedUrl, to: localURL)
31+
} catch {
32+
return
33+
}
34+
35+
guard let attachment = try? UNNotificationAttachment(identifier: identifier, url: localURL, options: nil) else {
36+
return
37+
}
38+
39+
content.attachments = [attachment]
40+
}
41+
task.resume()
42+
}
43+
44+
func addMessageAttachments(
45+
message: ChatMessage,
46+
content: UNMutableNotificationContent,
47+
completion: @escaping (UNMutableNotificationContent) -> Void
48+
) {
49+
if let imageURL = message.author.imageURL {
50+
addAttachments(url: imageURL, content: content) {
51+
completion($0)
52+
}
53+
return
54+
}
55+
if let attachment = message.imageAttachments.first {
56+
addAttachments(url: attachment.imageURL, content: content) {
57+
completion($0)
58+
}
59+
return
60+
}
61+
}
62+
63+
override func didReceive(
64+
_ request: UNNotificationRequest,
65+
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
66+
) {
67+
self.contentHandler = contentHandler
68+
self.request = request
69+
70+
guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
71+
return
72+
}
73+
74+
guard let userId = UserDefaults.standard.string(forKey: currentUserIdRegisteredForPush), let userCredentials = UserCredentials.builtInUsersByID(id: userId) else {
75+
return
76+
}
77+
78+
var config = ChatClientConfig(apiKey: .init(apiKeyString))
79+
config.isLocalStorageEnabled = true
80+
config.applicationGroupIdentifier = applicationGroupIdentifier
81+
82+
let client = ChatClient(config: config)
83+
client.setToken(token: Token(stringLiteral: userCredentials.token))
84+
85+
let chatHandler = ChatRemoteNotificationHandler(client: client, content: content)
86+
87+
let chatNotification = chatHandler.handleNotification { chatContent in
88+
switch chatContent {
89+
case let .message(messageNotification):
90+
content.title = (messageNotification.message.author.name ?? "somebody") + (" on \(messageNotification.channel?.name ?? "a conversation with you")")
91+
content.subtitle = ""
92+
content.body = messageNotification.message.text
93+
self.addMessageAttachments(message:messageNotification.message, content: content) {
94+
contentHandler($0)
95+
}
96+
default:
97+
content.title = "You received an update to one conversation"
98+
contentHandler(content)
99+
}
100+
}
101+
102+
if !chatNotification {
103+
/// this was not a notification from Stream Chat
104+
/// perform any other transformation to the notification if needed
105+
}
106+
}
107+
108+
override func serviceExtensionTimeWillExpire() {
109+
// Called just before the extension will be terminated by the system.
110+
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
111+
if let contentHandler = contentHandler, let bestAttemptContent = request?.content.mutableCopy() as? UNMutableNotificationContent {
112+
contentHandler(bestAttemptContent)
113+
}
114+
}
115+
}

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ var streamChatSourcesExcluded: [String] { [
9292
"WebSocketClient/Events/EventDecoder_Tests.swift",
9393
"WebSocketClient/Engine/WebSocketEngine_Mock.swift",
9494
"WebSocketClient/WebSocketPingController_Tests.swift",
95+
"APIClient/ChatPushNotificationContent_Tests.swift",
9596
"APIClient/HTTPHeader_Tests.swift",
9697
"APIClient/Endpoints/GuestEndpoints_Tests.swift",
9798
"APIClient/Endpoints/Payloads/CustomDataHashMap_Tests.swift",

0 commit comments

Comments
 (0)