mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 03:48:27 +00:00
Navigate to the Pinned events timeline (#3139)
This commit is contained in:
@@ -315,11 +315,15 @@
|
||||
"screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL";
|
||||
"screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call.";
|
||||
"screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address.";
|
||||
"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here.";
|
||||
"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered";
|
||||
"screen_pinned_timeline_screen_title_empty" = "Pinned messages";
|
||||
"screen_room_mentions_at_room_subtitle" = "Notify the whole room";
|
||||
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
|
||||
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
|
||||
"screen_room_pinned_banner_loading_description" = "Loading message…";
|
||||
"screen_room_pinned_banner_view_all_button_title" = "View All";
|
||||
"screen_room_details_pinned_events_row_title" = "Pinned messages";
|
||||
"screen_account_provider_change" = "Change account provider";
|
||||
"screen_account_provider_form_hint" = "Homeserver address";
|
||||
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
|
||||
|
||||
@@ -194,6 +194,22 @@
|
||||
<string>Wrong PIN. You have %1$d more chances</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_pinned_timeline_screen_title</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d Pinned message</string>
|
||||
<key>other</key>
|
||||
<string>%1$d Pinned messages</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_room_member_list_header_title</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -72,7 +72,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
appHooks.configure()
|
||||
|
||||
windowManager = WindowManager(appDelegate: appDelegate)
|
||||
appMediator = AppMediator(windowManager: windowManager)
|
||||
let networkMonitor = NetworkMonitor()
|
||||
appMediator = AppMediator(windowManager: windowManager, networkMonitor: networkMonitor)
|
||||
|
||||
let appSettings = appHooks.appSettingsHook.configure(AppSettings())
|
||||
|
||||
@@ -102,7 +103,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
let keychainController = KeychainController(service: .sessions,
|
||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||
userSessionStore = UserSessionStore(keychainController: keychainController, appSettings: appSettings, appHooks: appHooks)
|
||||
userSessionStore = UserSessionStore(keychainController: keychainController, appSettings: appSettings, appHooks: appHooks, networkMonitor: networkMonitor)
|
||||
|
||||
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
|
||||
let appLockNavigationCoordinator = NavigationRootCoordinator()
|
||||
@@ -342,7 +343,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
private static func setupServiceLocator(appSettings: AppSettings, appHooks: AppHooks) {
|
||||
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
ServiceLocator.shared.register(networkMonitor: NetworkMonitor())
|
||||
ServiceLocator.shared.register(bugReportService: BugReportService(withBaseURL: appSettings.bugReportServiceBaseURL,
|
||||
applicationId: appSettings.bugReportApplicationId,
|
||||
sdkGitSHA: sdkGitSha(),
|
||||
@@ -682,7 +682,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
private func observeNetworkState() {
|
||||
let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification"
|
||||
ServiceLocator.shared.networkMonitor
|
||||
appMediator.networkMonitor
|
||||
.reachabilityPublisher
|
||||
.sink { reachability in
|
||||
MXLog.info("Reachability changed to \(reachability)")
|
||||
@@ -851,12 +851,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
.loadingStatePublisher
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { state in
|
||||
.sink { [weak self] state in
|
||||
let toastIdentifier = "StaleDataIndicator"
|
||||
|
||||
switch state {
|
||||
case .loading:
|
||||
if ServiceLocator.shared.networkMonitor.reachabilityPublisher.value == .reachable {
|
||||
if self?.appMediator.networkMonitor.reachabilityPublisher.value == .reachable {
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: toastIdentifier, type: .toast(progress: .indeterminate), title: L10n.commonSyncing, persistent: true))
|
||||
}
|
||||
case .notLoading:
|
||||
|
||||
@@ -19,9 +19,11 @@ import UIKit
|
||||
|
||||
class AppMediator: AppMediatorProtocol {
|
||||
let windowManager: WindowManagerProtocol
|
||||
let networkMonitor: NetworkMonitorProtocol
|
||||
|
||||
init(windowManager: WindowManagerProtocol) {
|
||||
init(windowManager: WindowManagerProtocol, networkMonitor: NetworkMonitorProtocol) {
|
||||
self.windowManager = windowManager
|
||||
self.networkMonitor = networkMonitor
|
||||
}
|
||||
|
||||
// UIApplication.State won't update if we store this e.g. in the constructor
|
||||
|
||||
@@ -21,6 +21,7 @@ import UIKit
|
||||
@MainActor
|
||||
protocol AppMediatorProtocol {
|
||||
var windowManager: WindowManagerProtocol { get }
|
||||
var networkMonitor: NetworkMonitorProtocol { get }
|
||||
|
||||
var appState: UIApplication.State { get }
|
||||
|
||||
|
||||
@@ -33,12 +33,6 @@ class ServiceLocator {
|
||||
settings = appSettings
|
||||
}
|
||||
|
||||
private(set) var networkMonitor: NetworkMonitorProtocol!
|
||||
|
||||
func register(networkMonitor: NetworkMonitorProtocol) {
|
||||
self.networkMonitor = networkMonitor
|
||||
}
|
||||
|
||||
private(set) var analytics: AnalyticsService!
|
||||
|
||||
func register(analytics: AnalyticsService) {
|
||||
|
||||
@@ -325,7 +325,19 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .pollForm
|
||||
case (.pollForm, .dismissPollForm):
|
||||
return .room
|
||||
|
||||
|
||||
case (.room, .presentPinnedEventsTimeline):
|
||||
return .pinnedEventsTimeline(previousState: .room)
|
||||
case (.roomDetails(let isRoot), .presentPinnedEventsTimeline):
|
||||
return .pinnedEventsTimeline(previousState: .details(isRoot: isRoot))
|
||||
case (.pinnedEventsTimeline(let previousState), .dismissPinnedEventsTimeline):
|
||||
switch previousState {
|
||||
case .room:
|
||||
return .room
|
||||
case .details(let isRoot):
|
||||
return .roomDetails(isRoot: isRoot)
|
||||
}
|
||||
|
||||
case (.roomDetails, .presentPollsHistory):
|
||||
return .pollsHistory
|
||||
case (.pollsHistory, .dismissPollsHistory):
|
||||
@@ -457,11 +469,21 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
presentPollForm(mode: mode)
|
||||
case (.pollForm, .dismissPollForm, .room):
|
||||
break
|
||||
|
||||
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||
presentPinnedEventsTimeline()
|
||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
|
||||
break
|
||||
|
||||
case (.roomDetails, .presentPollsHistory, .pollsHistory):
|
||||
presentPollsHistory()
|
||||
case (.pollsHistory, .dismissPollsHistory, .roomDetails):
|
||||
break
|
||||
|
||||
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||
presentPinnedEventsTimeline()
|
||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
|
||||
break
|
||||
|
||||
case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm):
|
||||
presentPollForm(mode: mode)
|
||||
@@ -595,6 +617,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentMessageForwarding(forwardingItem: forwardingItem))
|
||||
case .presentCallScreen:
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentPinnedEventsTimeline:
|
||||
stateMachine.tryEvent(.presentPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -693,7 +717,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
analyticsService: analytics,
|
||||
userIndicatorController: userIndicatorController,
|
||||
notificationSettings: userSession.clientProxy.notificationSettings,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()))
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
appMediator: appMediator)
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
@@ -715,6 +740,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentRolesAndPermissionsScreen)
|
||||
case .presentCall:
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentPinnedEventsTimeline:
|
||||
stateMachine.tryEvent(.presentPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -937,6 +964,28 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self?.stateMachine.tryEvent(.dismissMapNavigator)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentPinnedEventsTimeline() {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init())
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
stackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentPollForm(mode: PollFormMode) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
@@ -1356,6 +1405,7 @@ private extension RoomFlowCoordinator {
|
||||
case pollsHistory
|
||||
case pollsHistoryForm
|
||||
case rolesAndPermissions
|
||||
case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource)
|
||||
|
||||
/// A child flow is in progress.
|
||||
case presentingChild(childRoomID: String, previousState: State)
|
||||
@@ -1425,6 +1475,9 @@ private extension RoomFlowCoordinator {
|
||||
case presentRolesAndPermissionsScreen
|
||||
case dismissRolesAndPermissionsScreen
|
||||
|
||||
case presentPinnedEventsTimeline
|
||||
case dismissPinnedEventsTimeline
|
||||
|
||||
// Child room flow events
|
||||
case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint)
|
||||
case dismissChildFlow
|
||||
@@ -1447,3 +1500,8 @@ private extension Result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum PinnedEventsTimelineSource: Hashable {
|
||||
case room
|
||||
case details(isRoot: Bool)
|
||||
}
|
||||
|
||||
@@ -602,7 +602,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentSecureBackupLogoutConfirmationScreen() {
|
||||
let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor))
|
||||
appMediator: appMediator))
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
|
||||
@@ -1237,6 +1237,18 @@ internal enum L10n {
|
||||
}
|
||||
/// Be in your element
|
||||
internal static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") }
|
||||
/// Press on a message and choose “%1$@” to include here.
|
||||
internal static func screenPinnedTimelineEmptyStateDescription(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_pinned_timeline_empty_state_description", String(describing: p1))
|
||||
}
|
||||
/// Pin important messages so that they can be easily discovered
|
||||
internal static var screenPinnedTimelineEmptyStateHeadline: String { return L10n.tr("Localizable", "screen_pinned_timeline_empty_state_headline") }
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
internal static func screenPinnedTimelineScreenTitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_pinned_timeline_screen_title", p1)
|
||||
}
|
||||
/// Pinned messages
|
||||
internal static var screenPinnedTimelineScreenTitleEmpty: String { return L10n.tr("Localizable", "screen_pinned_timeline_screen_title_empty") }
|
||||
/// Can't find any ongoing polls.
|
||||
internal static var screenPollsHistoryEmptyOngoing: String { return L10n.tr("Localizable", "screen_polls_history_empty_ongoing") }
|
||||
/// Can't find any past polls.
|
||||
@@ -1535,6 +1547,8 @@ internal enum L10n {
|
||||
internal static var screenRoomDetailsNotificationModeDefault: String { return L10n.tr("Localizable", "screen_room_details_notification_mode_default") }
|
||||
/// Notifications
|
||||
internal static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") }
|
||||
/// Pinned messages
|
||||
internal static var screenRoomDetailsPinnedEventsRowTitle: String { return L10n.tr("Localizable", "screen_room_details_pinned_events_row_title") }
|
||||
/// Roles and permissions
|
||||
internal static var screenRoomDetailsRolesAndPermissions: String { return L10n.tr("Localizable", "screen_room_details_roles_and_permissions") }
|
||||
/// Room name
|
||||
|
||||
@@ -23,6 +23,7 @@ extension AppMediatorMock {
|
||||
mock.underlyingAppState = .active
|
||||
mock.requestAuthorizationIfNeededUnderlyingReturnValue = true
|
||||
mock.underlyingWindowManager = WindowManagerMock()
|
||||
mock.underlyingNetworkMonitor = NetworkMonitorMock.default
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
@@ -808,6 +808,11 @@ class AppMediatorMock: AppMediatorProtocol {
|
||||
set(value) { underlyingWindowManager = value }
|
||||
}
|
||||
var underlyingWindowManager: WindowManagerProtocol!
|
||||
var networkMonitor: NetworkMonitorProtocol {
|
||||
get { return underlyingNetworkMonitor }
|
||||
set(value) { underlyingNetworkMonitor = value }
|
||||
}
|
||||
var underlyingNetworkMonitor: NetworkMonitorProtocol!
|
||||
var appState: UIApplication.State {
|
||||
get { return underlyingAppState }
|
||||
set(value) { underlyingAppState = value }
|
||||
|
||||
@@ -40,6 +40,7 @@ struct RoomProxyMockConfiguration {
|
||||
var canUserInvite = true
|
||||
var canUserTriggerRoomNotification = false
|
||||
var canUserJoinCall = true
|
||||
var canUserPin = true
|
||||
|
||||
var shouldUseAutoUpdatingTimeline = false
|
||||
}
|
||||
@@ -135,6 +136,7 @@ extension RoomProxyMock {
|
||||
}
|
||||
canUserTriggerRoomNotificationUserIDReturnValue = .success(configuration.canUserTriggerRoomNotification)
|
||||
canUserJoinCallUserIDReturnValue = .success(configuration.canUserJoinCall)
|
||||
canUserPinOrUnpinUserIDReturnValue = .success(configuration.canUserPin)
|
||||
|
||||
kickUserReturnValue = .success(())
|
||||
banUserReturnValue = .success(())
|
||||
|
||||
-2
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a encryptionResetPassword remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a logViewer remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
-2
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a identityConfirmation remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
-2
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a identityConfirmed remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
struct PinnedEventsTimelineScreenCoordinatorParameters { }
|
||||
|
||||
enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||
case dismiss
|
||||
|
||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||
}
|
||||
|
||||
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: PinnedEventsTimelineScreenCoordinatorParameters
|
||||
private let viewModel: PinnedEventsTimelineScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<PinnedEventsTimelineScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: PinnedEventsTimelineScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = PinnedEventsTimelineScreenViewModel()
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(PinnedEventsTimelineScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
enum PinnedEventsTimelineScreenViewModelAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct PinnedEventsTimelineScreenViewState: BindableState {
|
||||
var title: String {
|
||||
// TODO: Implement the non empty case
|
||||
L10n.screenPinnedTimelineScreenTitleEmpty
|
||||
}
|
||||
}
|
||||
|
||||
enum PinnedEventsTimelineScreenViewAction {
|
||||
case close
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias PinnedEventsTimelineScreenViewModelType = StateStoreViewModel<PinnedEventsTimelineScreenViewState, PinnedEventsTimelineScreenViewAction>
|
||||
|
||||
class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelType, PinnedEventsTimelineScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: PinnedEventsTimelineScreenViewState())
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: PinnedEventsTimelineScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .close:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
|
||||
@MainActor
|
||||
protocol PinnedEventsTimelineScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> { get }
|
||||
var context: PinnedEventsTimelineScreenViewModelType.Context { get }
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Copyright 2022 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 Compound
|
||||
import SwiftUI
|
||||
|
||||
struct PinnedEventsTimelineScreen: View {
|
||||
@ObservedObject var context: PinnedEventsTimelineScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.navigationTitle(context.viewState.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.background(.compound.bgCanvasDefault)
|
||||
}
|
||||
|
||||
private var content: some View {
|
||||
// TODO: Implement switching between empty state and timeline
|
||||
VStack(spacing: 16) {
|
||||
HeroImage(icon: \.pin, style: .normal)
|
||||
Text(L10n.screenPinnedTimelineEmptyStateHeadline)
|
||||
.font(.compound.headingSMSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(L10n.screenPinnedTimelineEmptyStateDescription(L10n.actionPin))
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 48)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(L10n.actionClose) {
|
||||
context.send(viewAction: .close)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = PinnedEventsTimelineScreenViewModel()
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
PinnedEventsTimelineScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a qRCodeLogin remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
-2
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a roomChangePermissions remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a roomChangeRoles remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ struct RoomDetailsScreenCoordinatorParameters {
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let notificationSettings: NotificationSettingsProxyProtocol
|
||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
}
|
||||
|
||||
enum RoomDetailsScreenCoordinatorAction {
|
||||
@@ -36,6 +37,7 @@ enum RoomDetailsScreenCoordinatorAction {
|
||||
case presentPollsHistory
|
||||
case presentRolesAndPermissionsScreen
|
||||
case presentCall
|
||||
case presentPinnedEventsTimeline
|
||||
}
|
||||
|
||||
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -55,7 +57,9 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
analyticsService: parameters.analyticsService,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
notificationSettingsProxy: parameters.notificationSettings,
|
||||
attributedStringBuilder: parameters.attributedStringBuilder)
|
||||
attributedStringBuilder: parameters.attributedStringBuilder,
|
||||
appMediator: parameters.appMediator,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -82,6 +86,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentRolesAndPermissionsScreen)
|
||||
case .startCall:
|
||||
actionsSubject.send(.presentCall)
|
||||
case .displayPinnedEventsTimeline:
|
||||
actionsSubject.send(.presentPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -30,6 +30,7 @@ enum RoomDetailsScreenViewModelAction {
|
||||
case requestPollsHistoryPresentation
|
||||
case requestRolesAndPermissionsPresentation
|
||||
case startCall
|
||||
case displayPinnedEventsTimeline
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
@@ -52,6 +53,8 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
var canEditRolesOrPermissions = false
|
||||
var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading
|
||||
var canJoinCall = false
|
||||
var isPinningEnabled = false
|
||||
var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading
|
||||
|
||||
var canEdit: Bool {
|
||||
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
|
||||
@@ -194,6 +197,7 @@ enum RoomDetailsScreenViewAction {
|
||||
case toggleFavourite(isFavourite: Bool)
|
||||
case processTapRolesAndPermissions
|
||||
case processTapCall
|
||||
case processTapPinnedEvents
|
||||
}
|
||||
|
||||
enum RoomDetailsScreenViewShortcut {
|
||||
@@ -261,3 +265,26 @@ enum RoomDetailsScreenErrorType: Hashable {
|
||||
/// Leaving room has failed..
|
||||
case unknown
|
||||
}
|
||||
|
||||
enum RoomDetailsScreenPinnedEventsActionState {
|
||||
case loading
|
||||
case loaded(numberOfItems: Int)
|
||||
|
||||
var count: String {
|
||||
switch self {
|
||||
case .loading:
|
||||
return ""
|
||||
case .loaded(let numberOfItems):
|
||||
return "\(numberOfItems)"
|
||||
}
|
||||
}
|
||||
|
||||
var isLoading: Bool {
|
||||
switch self {
|
||||
case .loading:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,26 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private var dmRecipient: RoomMemberProxyProtocol?
|
||||
private var pinnedEventsTimelineProvider: RoomTimelineProviderProtocol? {
|
||||
didSet {
|
||||
guard let pinnedEventsTimelineProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
state.pinnedEventsActionState = .loaded(numberOfItems: pinnedEventsTimelineProvider.itemProxies.filter(\.isEvent).count)
|
||||
|
||||
pinnedEventsTimelineProvider.updatePublisher
|
||||
// When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing
|
||||
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
||||
.sink { [weak self] updatedItems, _ in
|
||||
self?.state.pinnedEventsActionState = .loaded(numberOfItems: updatedItems.filter(\.isEvent).count)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
private var actionsSubject: PassthroughSubject<RoomDetailsScreenViewModelAction, Never> = .init()
|
||||
|
||||
@@ -42,7 +60,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
analyticsService: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
notificationSettingsProxy: NotificationSettingsProxyProtocol,
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol,
|
||||
appMediator: AppMediatorProtocol,
|
||||
appSettings: AppSettings) {
|
||||
self.roomProxy = roomProxy
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
@@ -50,6 +70,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.notificationSettingsProxy = notificationSettingsProxy
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
self.appSettings = appSettings
|
||||
|
||||
let topic = attributedStringBuilder.fromPlain(roomProxy.topic)
|
||||
|
||||
@@ -63,6 +84,19 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
bindings: .init()),
|
||||
imageProvider: mediaProvider)
|
||||
|
||||
appSettings.$pinningEnabled
|
||||
.weakAssign(to: \.state.isPinningEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$pinningEnabled
|
||||
.combineLatest(appMediator.networkMonitor.reachabilityPublisher)
|
||||
.filter { $0.0 && $0.1 == .reachable }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.setupPinnedEventsTimelineProviderIfNeeded()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
let userID = roomProxy.ownUserID
|
||||
if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) {
|
||||
@@ -135,6 +169,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
actionsSubject.send(.requestRolesAndPermissionsPresentation)
|
||||
case .processTapCall:
|
||||
actionsSubject.send(.startCall)
|
||||
case .processTapPinnedEvents:
|
||||
actionsSubject.send(.displayPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +373,22 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPinnedEventsTimelineProviderIfNeeded() {
|
||||
guard pinnedEventsTimelineProvider == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
if pinnedEventsTimelineProvider == nil {
|
||||
pinnedEventsTimelineProvider = timelineProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AttributedString {
|
||||
|
||||
@@ -198,6 +198,16 @@ struct RoomDetailsScreen: View {
|
||||
context.send(viewAction: .toggleFavourite(isFavourite: newValue))
|
||||
}
|
||||
|
||||
if context.viewState.isPinningEnabled {
|
||||
ListRow(label: .default(title: L10n.screenRoomDetailsPinnedEventsRowTitle,
|
||||
icon: \.pin),
|
||||
details: context.viewState.pinnedEventsActionState.isLoading ? .isWaiting(true) : .title(context.viewState.pinnedEventsActionState.count),
|
||||
kind: context.viewState.pinnedEventsActionState.isLoading ? .label : .navigationLink(action: {
|
||||
context.send(viewAction: .processTapPinnedEvents)
|
||||
}))
|
||||
.disabled(context.viewState.pinnedEventsActionState.isLoading)
|
||||
}
|
||||
|
||||
if context.viewState.canEditRolesOrPermissions, context.viewState.dmRecipient == nil {
|
||||
ListRow(label: .default(title: L10n.screenRoomDetailsRolesAndPermissions,
|
||||
icon: \.admin),
|
||||
@@ -320,6 +330,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
notificationSettingsProxyMockConfiguration.roomMode.isDefault = false
|
||||
let notificationSettingsProxy = NotificationSettingsProxyMock(with: notificationSettingsProxyMockConfiguration)
|
||||
let appSettings = AppSettings()
|
||||
appSettings.pinningEnabled = true
|
||||
|
||||
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
@@ -327,7 +338,9 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
notificationSettingsProxy: notificationSettingsProxy,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()))
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: appSettings)
|
||||
}()
|
||||
|
||||
static let dmRoomViewModel = {
|
||||
@@ -345,6 +358,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
members: members))
|
||||
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
|
||||
let appSettings = AppSettings()
|
||||
appSettings.pinningEnabled = true
|
||||
|
||||
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
@@ -352,7 +366,9 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
notificationSettingsProxy: notificationSettingsProxy,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()))
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: appSettings)
|
||||
}()
|
||||
|
||||
static let simpleRoomViewModel = {
|
||||
@@ -369,6 +385,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
members: members))
|
||||
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
|
||||
let appSettings = AppSettings()
|
||||
appSettings.pinningEnabled = true
|
||||
|
||||
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
@@ -376,7 +393,9 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
notificationSettingsProxy: notificationSettingsProxy,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()))
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: appSettings)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
-2
@@ -14,8 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a roomRolesAndPermissions remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ enum RoomScreenCoordinatorAction {
|
||||
case presentRoomMemberDetails(userID: String)
|
||||
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case presentCallScreen
|
||||
case presentPinnedEventsTimeline
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -67,7 +68,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: parameters.appMediator,
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
@@ -126,6 +126,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
composerViewModel.process(roomAction: action)
|
||||
case .displayCallScreen:
|
||||
actionsSubject.send(.presentCallScreen)
|
||||
case .displayPinnedEventsTimeline:
|
||||
actionsSubject.send(.presentPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -33,6 +33,7 @@ enum RoomScreenViewModelAction {
|
||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||
case composer(action: RoomScreenComposerAction)
|
||||
case displayCallScreen
|
||||
case displayPinnedEventsTimeline
|
||||
}
|
||||
|
||||
enum RoomScreenComposerMode: Equatable {
|
||||
|
||||
@@ -33,7 +33,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
private let timelineController: RoomTimelineControllerProtocol
|
||||
private let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let networkMonitor: NetworkMonitorProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let analyticsService: AnalyticsService
|
||||
@@ -50,7 +49,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
private var paginateBackwardsTask: Task<Void, Never>?
|
||||
private var paginateForwardsTask: Task<Void, Never>?
|
||||
private var pinnedEventsTimelineProviderSetupTask: Task<Void, Never>?
|
||||
|
||||
private var pinnedEventsTimelineProvider: RoomTimelineProviderProtocol? {
|
||||
didSet {
|
||||
@@ -77,7 +75,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
mediaPlayerProvider: MediaPlayerProviderProtocol,
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
networkMonitor: NetworkMonitorProtocol,
|
||||
appMediator: AppMediatorProtocol,
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsService) {
|
||||
@@ -88,7 +85,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
self.analyticsService = analyticsService
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appMediator = appMediator
|
||||
self.networkMonitor = networkMonitor
|
||||
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
|
||||
|
||||
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
||||
@@ -148,8 +144,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.canJoinCall = permission
|
||||
}
|
||||
}
|
||||
|
||||
setupPinnedEventsTimelineProviderIfNeeded()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -230,8 +224,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
state.pinnedEventsBannerState.previousPin()
|
||||
case .viewAllPins:
|
||||
// TODO: Implement
|
||||
break
|
||||
actionsSubject.send(.displayPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,12 +502,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
networkMonitor.reachabilityPublisher
|
||||
appSettings.$pinningEnabled
|
||||
.combineLatest(appMediator.networkMonitor.reachabilityPublisher)
|
||||
.filter { $0.0 && $0.1 == .reachable }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] networkState in
|
||||
if networkState == .reachable {
|
||||
self?.setupPinnedEventsTimelineProviderIfNeeded()
|
||||
}
|
||||
.sink { [weak self] _ in
|
||||
self?.setupPinnedEventsTimelineProviderIfNeeded()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
@@ -534,20 +527,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
private func setupPinnedEventsTimelineProviderIfNeeded() {
|
||||
guard pinnedEventsTimelineProvider == nil,
|
||||
pinnedEventsTimelineProviderSetupTask == nil else {
|
||||
guard pinnedEventsTimelineProvider == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
pinnedEventsTimelineProviderSetupTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
guard let pinnedEventsTimelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else {
|
||||
pinnedEventsTimelineProviderSetupTask = nil
|
||||
Task {
|
||||
guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
self.pinnedEventsTimelineProvider = pinnedEventsTimelineProvider
|
||||
if pinnedEventsTimelineProvider == nil {
|
||||
pinnedEventsTimelineProvider = timelineProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -953,7 +944,6 @@ extension RoomScreenViewModel {
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
@@ -58,7 +58,6 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
@@ -214,7 +214,6 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
@@ -96,7 +96,6 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
-1
@@ -101,7 +101,6 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
@@ -86,7 +86,6 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ import SwiftUI
|
||||
|
||||
struct SecureBackupLogoutConfirmationScreenCoordinatorParameters {
|
||||
let secureBackupController: SecureBackupControllerProtocol
|
||||
let networkMonitor: NetworkMonitorProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
}
|
||||
|
||||
enum SecureBackupLogoutConfirmationScreenCoordinatorAction {
|
||||
@@ -39,7 +39,7 @@ final class SecureBackupLogoutConfirmationScreenCoordinator: CoordinatorProtocol
|
||||
|
||||
init(parameters: SecureBackupLogoutConfirmationScreenCoordinatorParameters) {
|
||||
viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: parameters.secureBackupController,
|
||||
networkMonitor: parameters.networkMonitor)
|
||||
appMediator: parameters.appMediator)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
+5
-5
@@ -21,7 +21,7 @@ typealias SecureBackupLogoutConfirmationScreenViewModelType = StateStoreViewMode
|
||||
|
||||
class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmationScreenViewModelType, SecureBackupLogoutConfirmationScreenViewModelProtocol {
|
||||
private let secureBackupController: SecureBackupControllerProtocol
|
||||
private let networkMonitor: NetworkMonitorProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
|
||||
// periphery:ignore - auto cancels when reassigned
|
||||
@CancellableTask
|
||||
@@ -32,13 +32,13 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(secureBackupController: SecureBackupControllerProtocol, networkMonitor: NetworkMonitorProtocol) {
|
||||
init(secureBackupController: SecureBackupControllerProtocol, appMediator: AppMediatorProtocol) {
|
||||
self.secureBackupController = secureBackupController
|
||||
self.networkMonitor = networkMonitor
|
||||
self.appMediator = appMediator
|
||||
|
||||
super.init(initialViewState: .init(mode: .saveRecoveryKey))
|
||||
|
||||
networkMonitor.reachabilityPublisher
|
||||
appMediator.networkMonitor.reachabilityPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] reachability in
|
||||
guard let self,
|
||||
@@ -75,7 +75,7 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat
|
||||
|
||||
private func attemptLogout() {
|
||||
if state.mode == .saveRecoveryKey {
|
||||
state.mode = networkMonitor.reachabilityPublisher.value == .reachable ? .backupOngoing : .offline
|
||||
state.mode = appMediator.networkMonitor.reachabilityPublisher.value == .reachable ? .backupOngoing : .offline
|
||||
|
||||
keyUploadWaitingTask = Task {
|
||||
var result = await secureBackupController.waitForKeyBackupUpload()
|
||||
|
||||
+4
-1
@@ -126,7 +126,10 @@ struct SecureBackupLogoutConfirmationScreen_Previews: PreviewProvider, TestableP
|
||||
let networkMonitor = NetworkMonitorMock()
|
||||
networkMonitor.underlyingReachabilityPublisher = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable).asCurrentValuePublisher()
|
||||
|
||||
let appMediator = AppMediatorMock()
|
||||
appMediator.underlyingNetworkMonitor = networkMonitor
|
||||
|
||||
return SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController,
|
||||
networkMonitor: networkMonitor)
|
||||
appMediator: appMediator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,38 @@ class RoomProxy: RoomProxyProtocol {
|
||||
private let roomListItem: RoomListItemProtocol
|
||||
private let room: RoomProtocol
|
||||
let timeline: TimelineProxyProtocol
|
||||
|
||||
private var innerPinnedEventsTimeline: TimelineProxyProtocol?
|
||||
private var innerPinnedEventsTimelineTask: Task<TimelineProxyProtocol?, Never>?
|
||||
var pinnedEventsTimeline: TimelineProxyProtocol? {
|
||||
get async {
|
||||
// Check if is alrrady available.
|
||||
if let innerPinnedEventsTimeline {
|
||||
return innerPinnedEventsTimeline
|
||||
// Otherwise check if there is already a task loading it, and wait for it.
|
||||
} else if let innerPinnedEventsTimelineTask,
|
||||
let value = await innerPinnedEventsTimelineTask.value {
|
||||
return value
|
||||
// Else create and store a new task to load it and wait for it.
|
||||
} else {
|
||||
do {
|
||||
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: false)
|
||||
await timeline.subscribeForUpdates()
|
||||
innerPinnedEventsTimeline = timeline
|
||||
return timeline
|
||||
} catch {
|
||||
MXLog.error("Failed creating pinned events timeline with error: \(error)")
|
||||
return nil
|
||||
let task = Task<TimelineProxyProtocol?, Never> { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: false)
|
||||
await timeline.subscribeForUpdates()
|
||||
innerPinnedEventsTimeline = timeline
|
||||
return timeline
|
||||
} catch {
|
||||
MXLog.error("Failed creating pinned events timeline with error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
innerPinnedEventsTimelineTask = task
|
||||
return await task.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ enum TimelineItemProxy {
|
||||
self = .unknown(item)
|
||||
}
|
||||
}
|
||||
|
||||
var isEvent: Bool {
|
||||
switch self {
|
||||
case .event:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The delivery status for the item.
|
||||
|
||||
@@ -21,6 +21,7 @@ import MatrixRustSDK
|
||||
class UserSessionStore: UserSessionStoreProtocol {
|
||||
private let keychainController: KeychainControllerProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let networkMonitor: NetworkMonitorProtocol
|
||||
private let appHooks: AppHooks
|
||||
private let matrixSDKStateKey = "matrix-sdk-state"
|
||||
|
||||
@@ -31,10 +32,14 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
var clientSessionDelegate: ClientSessionDelegate { keychainController }
|
||||
|
||||
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings, appHooks: AppHooks) {
|
||||
init(keychainController: KeychainControllerProtocol,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks,
|
||||
networkMonitor: NetworkMonitorProtocol) {
|
||||
self.keychainController = keychainController
|
||||
self.appSettings = appSettings
|
||||
self.appHooks = appHooks
|
||||
self.networkMonitor = networkMonitor
|
||||
}
|
||||
|
||||
/// Deletes all data stored in the shared container and keychain
|
||||
@@ -146,8 +151,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
private func setupProxyForClient(_ client: Client) async -> ClientProxyProtocol {
|
||||
await ClientProxy(client: client,
|
||||
networkMonitor: ServiceLocator.shared.networkMonitor,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
networkMonitor: networkMonitor,
|
||||
appSettings: appSettings)
|
||||
}
|
||||
|
||||
private func deleteSessionDirectory(for credentials: KeychainCredentials) {
|
||||
|
||||
@@ -47,7 +47,6 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, SecureWindowManagerDelegate
|
||||
AppSettings.resetAllSettings()
|
||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||
ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
|
||||
ServiceLocator.shared.register(networkMonitor: NetworkMonitorMock.default)
|
||||
|
||||
let analyticsClient = AnalyticsClientMock()
|
||||
analyticsClient.isRunning = false
|
||||
|
||||
@@ -27,7 +27,6 @@ class UnitTestsAppCoordinator: AppCoordinatorProtocol {
|
||||
AppSettings.resetAllSettings()
|
||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||
ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
|
||||
ServiceLocator.shared.register(networkMonitor: NetworkMonitorMock.default)
|
||||
|
||||
let analyticsClient = AnalyticsClientMock()
|
||||
analyticsClient.isRunning = false
|
||||
|
||||
Reference in New Issue
Block a user