Updated `setPusher` function (#684)

* updated set_pusher function from the SDK

* this fixes a crash and allows for the navigation to work by using the threadIdentifier of the notification

* adding NCE target

* project setup completed with xcodegen

* no need for those ugly storyboards

* code improvement

* removing unused outlet

* mocks generated with the comment instead of the marker protocol

* updated stencil

* fixed unit tests

* updated swiftformat

* pr comments
This commit is contained in:
Mauro 2023-03-16 16:39:10 +01:00 committed by GitHub
parent 701581f4a1
commit 7544619a55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 245 additions and 154 deletions

View File

@ -473,11 +473,12 @@ extension AppCoordinator: NotificationManagerDelegate {
func notificationTapped(_ service: NotificationManagerProtocol, content: UNNotificationContent) async {
MXLog.info("[AppCoordinator] tappedNotification")
guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else {
// We store the room identifier into the thread identifier
guard !content.threadIdentifier.isEmpty else {
return
}
userSessionFlowCoordinator?.tryDisplayingRoomScreen(roomId: roomId)
userSessionFlowCoordinator?.tryDisplayingRoomScreen(roomId: content.threadIdentifier)
}
func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async {

View File

@ -2,7 +2,6 @@
// DO NOT EDIT
// swiftlint:disable all
import Combine
import MatrixRustSDK
class BugReportServiceMock: BugReportServiceProtocol {
@ -11,6 +10,7 @@ class BugReportServiceMock: BugReportServiceProtocol {
set(value) { underlyingCrashedLastRun = value }
}
var underlyingCrashedLastRun: Bool!
//MARK: - crash
var crashCallsCount = 0
@ -18,6 +18,7 @@ class BugReportServiceMock: BugReportServiceProtocol {
return crashCallsCount > 0
}
var crashClosure: (() -> Void)?
func crash() {
crashCallsCount += 1
crashClosure?()
@ -33,6 +34,7 @@ class BugReportServiceMock: BugReportServiceProtocol {
var submitBugReportProgressListenerReceivedInvocations: [(bugReport: BugReport, progressListener: ProgressListener?)] = []
var submitBugReportProgressListenerReturnValue: SubmitBugReportResponse!
var submitBugReportProgressListenerClosure: ((BugReport, ProgressListener?) async throws -> SubmitBugReportResponse)?
func submitBugReport(_ bugReport: BugReport, progressListener: ProgressListener?) async throws -> SubmitBugReportResponse {
if let error = submitBugReportProgressListenerThrowableError {
throw error
@ -58,6 +60,7 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
set(value) { underlyingIsVerified = value }
}
var underlyingIsVerified: Bool!
//MARK: - requestVerification
var requestVerificationCallsCount = 0
@ -66,6 +69,7 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
var requestVerificationReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var requestVerificationClosure: (() async -> Result<Void, SessionVerificationControllerProxyError>)?
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
requestVerificationCallsCount += 1
if let requestVerificationClosure = requestVerificationClosure {
@ -82,6 +86,7 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
var startSasVerificationReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var startSasVerificationClosure: (() async -> Result<Void, SessionVerificationControllerProxyError>)?
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
startSasVerificationCallsCount += 1
if let startSasVerificationClosure = startSasVerificationClosure {
@ -98,6 +103,7 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
var approveVerificationReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var approveVerificationClosure: (() async -> Result<Void, SessionVerificationControllerProxyError>)?
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
approveVerificationCallsCount += 1
if let approveVerificationClosure = approveVerificationClosure {
@ -114,6 +120,7 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
var declineVerificationReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var declineVerificationClosure: (() async -> Result<Void, SessionVerificationControllerProxyError>)?
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
declineVerificationCallsCount += 1
if let declineVerificationClosure = declineVerificationClosure {
@ -130,6 +137,7 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
var cancelVerificationReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var cancelVerificationClosure: (() async -> Result<Void, SessionVerificationControllerProxyError>)?
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
cancelVerificationCallsCount += 1
if let cancelVerificationClosure = cancelVerificationClosure {
@ -139,5 +147,4 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
}
}
// swiftlint:enable all

View File

@ -14,6 +14,15 @@
// limitations under the License.
//
// This protocol is used only as a marker
// to mark protocols that can be auto-mocked by Sourcery
protocol AutoMockable { }
import Foundation
extension Dictionary {
var jsonString: String? {
guard let data = try? JSONSerialization.data(withJSONObject: self,
options: [.fragmentsAllowed, .sortedKeys]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}

View File

@ -31,7 +31,8 @@ struct SubmitBugReportResponse: Decodable {
var reportUrl: String
}
protocol BugReportServiceProtocol: AutoMockable {
// sourcery: AutoMockable
protocol BugReportServiceProtocol {
var crashedLastRun: Bool { get }
func crash()

View File

@ -212,30 +212,15 @@ class ClientProxy: ClientProxyProtocol {
}
}
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: URL?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws {
// let defaultPayloadString = jsonString(from: defaultPayload)
// try await Task.dispatch(on: .global()) {
// try self.client.setPusher(pushkey: pushkey,
// kind: kind?.rustValue,
// appId: appId,
// appDisplayName: appDisplayName,
// deviceDisplayName: deviceDisplayName,
// profileTag: profileTag,
// lang: lang,
// url: url,
// format: format?.rustValue,
// defaultPayload: defaultPayloadString)
// }
func setPusher(with configuration: PusherConfiguration) async throws {
try await Task.dispatch(on: .global()) {
try self.client.setPusher(identifiers: configuration.identifiers,
kind: configuration.kind,
appDisplayName: configuration.appDisplayName,
deviceDisplayName: configuration.deviceDisplayName,
profileTag: configuration.profileTag,
lang: configuration.lang)
}
}
// MARK: Private
@ -399,17 +384,6 @@ class ClientProxy: ClientProxyProtocol {
fileprivate func didReceiveSlidingSyncUpdate(summary: UpdateSummary) {
callbacks.send(.receivedSyncUpdate)
}
/// Convenience method to get the json string of an Encodable
private func jsonString(from dictionary: [AnyHashable: Any]?) -> String? {
guard let dictionary,
let data = try? JSONSerialization.data(withJSONObject: dictionary,
options: [.fragmentsAllowed]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
extension ClientProxy: MediaLoaderProtocol {

View File

@ -31,37 +31,24 @@ enum ClientProxyError: Error {
case failedLoadingMedia
}
enum PusherKind {
case http
case email
// var rustValue: MatrixRustSDK.PusherKind {
// switch self {
// case .http:
// return .http
// case .email:
// return .email
// }
// }
}
enum PushFormat {
case eventIdOnly
// var rustValue: MatrixRustSDK.PushFormat {
// switch self {
// case .eventIdOnly:
// return .eventIdOnly
// }
// }
}
enum SlidingSyncConstants {
static let initialTimelineLimit: UInt = 0
static let lastMessageTimelineLimit: UInt = 1
static let timelinePrecachingTimelineLimit: UInt = 20
}
/// This struct represents the configuration that we are using to register the application through Pusher to Sygnal
/// using the Matrix Rust SDK, more info here:
/// https://github.com/matrix-org/sygnal
struct PusherConfiguration {
let identifiers: PusherIdentifiers
let kind: PusherKind
let appDisplayName: String
let deviceDisplayName: String
let profileTag: String?
let lang: String
}
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
@ -97,15 +84,5 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func logout() async
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: URL?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws
func setPusher(with configuration: PusherConfiguration) async throws
}

View File

@ -89,40 +89,13 @@ class MockClientProxy: ClientProxyProtocol {
// no-op
}
var setPusherCalled = false
var setPusherErrorToThrow: Error?
var setPusherPushkey: String?
var setPusherKind: PusherKind?
var setPusherAppId: String?
var setPusherAppDisplayName: String?
var setPusherDeviceDisplayName: String?
var setPusherProfileTag: String?
var setPusherLang: String?
var setPusherUrl: URL?
var setPusherFormat: PushFormat?
var setPusherDefaultPayload: [AnyHashable: Any]?
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: URL?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws {
var setPusherArgument: PusherConfiguration?
var setPusherCalled = false
func setPusher(with configuration: PusherConfiguration) async throws {
if let setPusherErrorToThrow { throw setPusherErrorToThrow }
setPusherCalled = true
setPusherPushkey = pushkey
setPusherKind = kind
setPusherAppId = appId
setPusherAppDisplayName = appDisplayName
setPusherDeviceDisplayName = deviceDisplayName
setPusherProfileTag = profileTag
setPusherLang = lang
setPusherUrl = url
setPusherFormat = format
setPusherDefaultPayload = defaultPayload
setPusherArgument = configuration
}
}

View File

@ -84,16 +84,7 @@ class NotificationManager: NSObject, NotificationManagerProtocol {
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
do {
try await clientProxy.setPusher(pushkey: deviceToken.base64EncodedString(),
kind: .http,
appId: ServiceLocator.shared.settings.pusherAppId,
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
deviceDisplayName: UIDevice.current.name,
profileTag: pusherProfileTag(),
lang: Bundle.preferredLanguages.first ?? "en",
url: ServiceLocator.shared.settings.pushGatewayBaseURL,
format: .eventIdOnly,
defaultPayload: [
let defaultPayload = [
"aps": [
"mutable-content": 1,
"alert": [
@ -101,7 +92,17 @@ class NotificationManager: NSObject, NotificationManagerProtocol {
"loc-args": []
]
]
])
]
let configuration = await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(),
appId: ServiceLocator.shared.settings.pusherAppId),
kind: .http(data: .init(url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString,
format: .eventIdOnly,
defaultPayload: defaultPayload.jsonString)),
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
deviceDisplayName: UIDevice.current.name,
profileTag: pusherProfileTag(),
lang: Bundle.preferredLanguages.first ?? "en")
try await clientProxy.setPusher(with: configuration)
MXLog.info("[NotificationManager] set pusher succeeded")
return true
} catch {
@ -144,6 +145,7 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
return [.badge, .sound, .list, .banner]
}
@MainActor
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
switch response.actionIdentifier {

View File

@ -39,7 +39,8 @@ struct SessionVerificationEmoji: Hashable {
let description: String
}
protocol SessionVerificationControllerProxyProtocol: AutoMockable {
// sourcery: AutoMockable
protocol SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> { get }
var isVerified: Bool { get }

View File

@ -0,0 +1,30 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import UIKit
import UserNotifications
import UserNotificationsUI
class NotificationViewController: UIViewController, UNNotificationContentExtension {
override func viewDidLoad() {
super.viewDidLoad()
// Do any required interface initialization here.
}
func didReceive(_ notification: UNNotification) {
// Handle the received push notification
}
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>UNNotificationExtensionCategory</key>
<string>myNotificationCategory</string>
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<integer>1</integer>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.content-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>NCE.NotificationViewController</string>
</dict>
<key>appGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>baseBundleIdentifier</key>
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
<key>keychainAccessGroupIdentifier</key>
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
</dict>
</plist>

View File

@ -0,0 +1,66 @@
name: NCE
schemes:
NCE:
analyze:
config: Debug
archive:
config: Release
build:
targets:
NCE:
- running
- testing
- profiling
- analyzing
- archiving
profile:
config: Release
run:
askForAppToLaunch: true
config: Debug
debugEnabled: false
disableMainThreadChecker: false
launchAutomaticallySubstyle: 2
test:
config: Debug
disableMainThreadChecker: false
targets:
NCE:
type: app-extension
platform: iOS
dependencies:
- package: MatrixRustSDK
info:
path: ../SupportingFiles/Info.plist
properties:
CFBundleDisplayName: $(PRODUCT_NAME)
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER)
keychainAccessGroupIdentifier: $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
NSExtension:
NSExtensionPointIdentifier: com.apple.usernotifications.content-extension
NSExtensionPrincipalClass: NCE.NotificationViewController
NSExtensionAttributes:
UNNotificationExtensionCategory: myNotificationCategory
UNNotificationExtensionInitialContentSizeRatio: 1
settings:
base:
PRODUCT_NAME: NCE
PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.nce
MARKETING_VERSION: $(MARKETING_VERSION)
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
debug:
release:
sources:
- path: ../Sources
- path: ../SupportingFiles

View File

@ -132,6 +132,8 @@ extension NotificationItemProxy {
if let subtitle = subtitle {
notification.subtitle = subtitle
}
// We can store the room identifier into the thread identifier since it's used for notifications
// that belong to the same group
notification.threadIdentifier = roomId
notification.categoryIdentifier = NotificationConstants.Category.reply
notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil

View File

@ -1,5 +1,4 @@
// swiftlint:disable all
{% for import in argument.autoMockableImports %}
import {{ import }}
{% endfor %}
@ -67,6 +66,7 @@ import {{ import }}
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}ReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ method.returnTypeName }}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '!' if not method.isOptionalReturnType }}
{% endif %}
{% call methodClosureDeclaration method %}
{% if method.isInitializer %}
{% call accessLevel method.accessLevel %}required {{ method.name }} {
{% call methodReceivedParameters method %}
@ -127,7 +127,8 @@ import {{ import }}
get { return {% call underlyingMockedVariableName variable %} }
set(value) { {% call underlyingMockedVariableName variable %} = value }
}
{% call accessLevel variable.readAccess %}var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}!
{% set wrappedTypeName %}{% if variable.typeName.isProtocolComposition %}({{ variable.typeName }}){% else %}{{ variable.typeName }}{% endif %}{% endset %}
{% call accessLevel variable.readAccess %}var {% call underlyingMockedVariableName variable %}: {{ wrappedTypeName }}!
{% endmacro %}
{% macro variableThrowableErrorDeclaration variable %}
{% call accessLevel variable.readAccess %}var {% call mockedVariableName variable %}ThrowableError: Error?
@ -146,6 +147,7 @@ import {{ import }}
{% call accessLevel variable.readAccess %}var {% call mockedVariableName variable %}Called: Bool {
return {% call mockedVariableName variable %}CallsCount > 0
}
{% call accessLevel variable.readAccess %}var {% call mockedVariableName variable %}: {{ variable.typeName }} {
get {% if variable.isAsync %}async {% endif %}{% if variable.throws %}throws {% endif %}{
{% if variable.throws %}
@ -181,10 +183,10 @@ import {{ import }}
{% endfor %}
}
{% endif %}
{% for method in type.allMethods|!definedInExtension %}
{% call mockMethod method %}
{% endfor %}
}
{% endif %}{% endfor %}
// swiftlint:enable all

View File

@ -56,16 +56,18 @@ final class NotificationManagerTests: XCTestCase {
func test_whenRegistered_pusherIsCalledWithCorrectValues() async throws {
let pushkeyData = Data("1234".utf8)
_ = await notificationManager.register(with: pushkeyData)
XCTAssertEqual(clientProxy.setPusherPushkey, pushkeyData.base64EncodedString())
XCTAssertEqual(clientProxy.setPusherAppId, settings?.pusherAppId)
XCTAssertEqual(clientProxy.setPusherKind, .http)
XCTAssertEqual(clientProxy.setPusherAppId, settings?.pusherAppId)
XCTAssertEqual(clientProxy.setPusherAppDisplayName, "\(InfoPlistReader.main.bundleDisplayName) (iOS)")
XCTAssertEqual(clientProxy.setPusherDeviceDisplayName, UIDevice.current.name)
XCTAssertNotNil(clientProxy.setPusherProfileTag)
XCTAssertEqual(clientProxy.setPusherLang, Bundle.preferredLanguages.first)
XCTAssertEqual(clientProxy.setPusherUrl, settings?.pushGatewayBaseURL)
XCTAssertEqual(clientProxy.setPusherFormat, .eventIdOnly)
XCTAssertEqual(clientProxy.setPusherArgument?.identifiers.pushkey, pushkeyData.base64EncodedString())
XCTAssertEqual(clientProxy.setPusherArgument?.identifiers.appId, settings?.pusherAppId)
XCTAssertEqual(clientProxy.setPusherArgument?.appDisplayName, "\(InfoPlistReader.main.bundleDisplayName) (iOS)")
XCTAssertEqual(clientProxy.setPusherArgument?.deviceDisplayName, UIDevice.current.name)
XCTAssertNotNil(clientProxy.setPusherArgument?.profileTag)
XCTAssertEqual(clientProxy.setPusherArgument?.lang, Bundle.preferredLanguages.first)
guard case let .http(data) = clientProxy.setPusherArgument?.kind else {
XCTFail("Http kind expected")
return
}
XCTAssertEqual(data.url, settings?.pushGatewayBaseURL.absoluteString)
XCTAssertEqual(data.format, .eventIdOnly)
let defaultPayload: [AnyHashable: Any] = [
"aps": [
"mutable-content": 1,
@ -75,8 +77,7 @@ final class NotificationManagerTests: XCTestCase {
]
]
]
let actualPayload = NSDictionary(dictionary: clientProxy.setPusherDefaultPayload ?? [:])
XCTAssertTrue(actualPayload.isEqual(to: defaultPayload))
XCTAssertEqual(data.defaultPayload, defaultPayload.jsonString)
}
func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async throws {

View File

@ -37,6 +37,7 @@ include:
- path: UITests/SupportingFiles/target.yml
- path: IntegrationTests/SupportingFiles/target.yml
- path: NSE/SupportingFiles/target.yml
- path: NCE/SupportingFiles/target.yml
packages:
MatrixRustSDK: