mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Check Application state before sending RR (#1960)
* check the state * better handling * protocol to mock the application state * test improvement
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension ApplicationMock {
|
||||
static var `default`: ApplicationProtocol {
|
||||
ApplicationMock(withState: .active,
|
||||
backgroundTimeRemaining: 10,
|
||||
allowTasks: true)
|
||||
}
|
||||
|
||||
static var mockBroken: ApplicationProtocol {
|
||||
ApplicationMock(withState: .inactive,
|
||||
backgroundTimeRemaining: 0,
|
||||
allowTasks: false)
|
||||
}
|
||||
|
||||
static var mockAboutToSuspend: ApplicationProtocol {
|
||||
ApplicationMock(withState: .background,
|
||||
backgroundTimeRemaining: 2,
|
||||
allowTasks: false)
|
||||
}
|
||||
|
||||
private static var bgTaskIdentifier = 0
|
||||
|
||||
convenience init(withState applicationState: UIApplication.State,
|
||||
backgroundTimeRemaining: TimeInterval,
|
||||
allowTasks: Bool) {
|
||||
self.init()
|
||||
|
||||
underlyingApplicationState = applicationState
|
||||
underlyingBackgroundTimeRemaining = backgroundTimeRemaining
|
||||
|
||||
beginBackgroundTaskExpirationHandlerClosure = { [weak self] handler in
|
||||
guard let self else {
|
||||
return .invalid
|
||||
}
|
||||
|
||||
guard allowTasks else {
|
||||
return .invalid
|
||||
}
|
||||
|
||||
return beginBackgroundTask(withName: nil, expirationHandler: handler)
|
||||
}
|
||||
|
||||
beginBackgroundTaskWithNameExpirationHandlerClosure = { _, handler in
|
||||
guard allowTasks else {
|
||||
return .invalid
|
||||
}
|
||||
Self.bgTaskIdentifier += 1
|
||||
|
||||
let identifier = UIBackgroundTaskIdentifier(rawValue: Self.bgTaskIdentifier)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
handler?()
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,6 +279,85 @@ class AppLockServiceMock: AppLockServiceProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
class ApplicationMock: ApplicationProtocol {
|
||||
var backgroundTimeRemaining: TimeInterval {
|
||||
get { return underlyingBackgroundTimeRemaining }
|
||||
set(value) { underlyingBackgroundTimeRemaining = value }
|
||||
}
|
||||
var underlyingBackgroundTimeRemaining: TimeInterval!
|
||||
var applicationState: UIApplication.State {
|
||||
get { return underlyingApplicationState }
|
||||
set(value) { underlyingApplicationState = value }
|
||||
}
|
||||
var underlyingApplicationState: UIApplication.State!
|
||||
|
||||
//MARK: - beginBackgroundTask
|
||||
|
||||
var beginBackgroundTaskExpirationHandlerCallsCount = 0
|
||||
var beginBackgroundTaskExpirationHandlerCalled: Bool {
|
||||
return beginBackgroundTaskExpirationHandlerCallsCount > 0
|
||||
}
|
||||
var beginBackgroundTaskExpirationHandlerReturnValue: UIBackgroundTaskIdentifier!
|
||||
var beginBackgroundTaskExpirationHandlerClosure: (((() -> Void)?) -> UIBackgroundTaskIdentifier)?
|
||||
|
||||
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
|
||||
beginBackgroundTaskExpirationHandlerCallsCount += 1
|
||||
if let beginBackgroundTaskExpirationHandlerClosure = beginBackgroundTaskExpirationHandlerClosure {
|
||||
return beginBackgroundTaskExpirationHandlerClosure(handler)
|
||||
} else {
|
||||
return beginBackgroundTaskExpirationHandlerReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - beginBackgroundTask
|
||||
|
||||
var beginBackgroundTaskWithNameExpirationHandlerCallsCount = 0
|
||||
var beginBackgroundTaskWithNameExpirationHandlerCalled: Bool {
|
||||
return beginBackgroundTaskWithNameExpirationHandlerCallsCount > 0
|
||||
}
|
||||
var beginBackgroundTaskWithNameExpirationHandlerReturnValue: UIBackgroundTaskIdentifier!
|
||||
var beginBackgroundTaskWithNameExpirationHandlerClosure: ((String?, (() -> Void)?) -> UIBackgroundTaskIdentifier)?
|
||||
|
||||
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
|
||||
beginBackgroundTaskWithNameExpirationHandlerCallsCount += 1
|
||||
if let beginBackgroundTaskWithNameExpirationHandlerClosure = beginBackgroundTaskWithNameExpirationHandlerClosure {
|
||||
return beginBackgroundTaskWithNameExpirationHandlerClosure(taskName, handler)
|
||||
} else {
|
||||
return beginBackgroundTaskWithNameExpirationHandlerReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - endBackgroundTask
|
||||
|
||||
var endBackgroundTaskCallsCount = 0
|
||||
var endBackgroundTaskCalled: Bool {
|
||||
return endBackgroundTaskCallsCount > 0
|
||||
}
|
||||
var endBackgroundTaskReceivedIdentifier: UIBackgroundTaskIdentifier?
|
||||
var endBackgroundTaskReceivedInvocations: [UIBackgroundTaskIdentifier] = []
|
||||
var endBackgroundTaskClosure: ((UIBackgroundTaskIdentifier) -> Void)?
|
||||
|
||||
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) {
|
||||
endBackgroundTaskCallsCount += 1
|
||||
endBackgroundTaskReceivedIdentifier = identifier
|
||||
endBackgroundTaskReceivedInvocations.append(identifier)
|
||||
endBackgroundTaskClosure?(identifier)
|
||||
}
|
||||
//MARK: - open
|
||||
|
||||
var openCallsCount = 0
|
||||
var openCalled: Bool {
|
||||
return openCallsCount > 0
|
||||
}
|
||||
var openReceivedUrl: URL?
|
||||
var openReceivedInvocations: [URL] = []
|
||||
var openClosure: ((URL) -> Void)?
|
||||
|
||||
func open(_ url: URL) {
|
||||
openCallsCount += 1
|
||||
openReceivedUrl = url
|
||||
openReceivedInvocations.append(url)
|
||||
openClosure?(url)
|
||||
}
|
||||
}
|
||||
class AudioConverterMock: AudioConverterProtocol {
|
||||
|
||||
//MARK: - convertToOpusOgg
|
||||
|
||||
@@ -65,7 +65,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
roomProxy: parameters.roomProxy,
|
||||
appSettings: parameters.appSettings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: UIApplication.shared)
|
||||
|
||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||
|
||||
@@ -32,6 +32,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let analytics: AnalyticsService
|
||||
private let application: ApplicationProtocol
|
||||
private unowned let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let notificationCenterProtocol: NotificationCenterProtocol
|
||||
private let voiceMessageRecorder: VoiceMessageRecorderProtocol
|
||||
@@ -48,6 +49,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
appSettings: AppSettings,
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
application: ApplicationProtocol,
|
||||
notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) {
|
||||
self.roomProxy = roomProxy
|
||||
self.timelineController = timelineController
|
||||
@@ -56,6 +58,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.notificationCenterProtocol = notificationCenterProtocol
|
||||
self.mediaPlayerProvider = mediaPlayerProvider
|
||||
self.application = application
|
||||
voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState(roomID: timelineController.roomID,
|
||||
@@ -327,16 +330,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
/// The ID of the newest item in the room that the user has seen.
|
||||
/// This includes both event based items and virtual items.
|
||||
private var lastReadItemID: TimelineItemIdentifier?
|
||||
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError> {
|
||||
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async {
|
||||
guard application.applicationState == .active else { return }
|
||||
|
||||
guard lastReadItemID != lastVisibleItemID,
|
||||
let eventItemID = eventBasedItem(nearest: lastVisibleItemID)
|
||||
else { return .success(()) }
|
||||
let eventItemID = eventBasedItem(nearest: lastVisibleItemID) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the item is newer than the item that was last marked as read.
|
||||
if let lastReadItemIndex = state.timelineViewState.timelineIDs.firstIndex(of: lastReadItemID?.timelineID ?? ""),
|
||||
let lastVisibleItemIndex = state.timelineViewState.timelineIDs.firstIndex(of: eventItemID.timelineID),
|
||||
lastReadItemIndex > lastVisibleItemIndex {
|
||||
return .success(())
|
||||
return
|
||||
}
|
||||
|
||||
// Update the last read item ID to avoid attempting duplicate requests.
|
||||
@@ -348,10 +354,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
switch await timelineController.sendReadReceipt(for: eventItemID) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure:
|
||||
return .failure(.generic)
|
||||
case .success():
|
||||
break
|
||||
case let .failure(error):
|
||||
MXLog.error("[TimelineViewController] Failed to send read receipt: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,7 +1034,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
private func openSystemSettings() {
|
||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
application.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1061,7 +1067,8 @@ extension RoomScreenViewModel {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
}
|
||||
|
||||
private struct ReplyInfo {
|
||||
|
||||
@@ -61,7 +61,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name", avatarURL: URL.picturesDirectory)),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
|
||||
RoomHeaderView(context: viewModel.context)
|
||||
.previewLayout(.sizeThatFits)
|
||||
@@ -76,7 +77,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
|
||||
RoomHeaderView(context: viewModel.context)
|
||||
.previewLayout(.sizeThatFits)
|
||||
|
||||
@@ -195,7 +195,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room", isCallOngoing: true)),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
+2
-1
@@ -65,7 +65,8 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
members: members)),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
|
||||
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
|
||||
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
||||
|
||||
@@ -86,7 +86,8 @@ struct UITimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -174,7 +174,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
application: ApplicationMock.default)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -17,16 +17,23 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ApplicationProtocol {
|
||||
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
|
||||
|
||||
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
|
||||
|
||||
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
|
||||
|
||||
func open(_ url: URL)
|
||||
|
||||
var backgroundTimeRemaining: TimeInterval { get }
|
||||
|
||||
var applicationState: UIApplication.State { get }
|
||||
}
|
||||
|
||||
extension UIApplication: ApplicationProtocol { }
|
||||
extension UIApplication: ApplicationProtocol {
|
||||
func open(_ url: URL) {
|
||||
open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user