mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Pinned Events Timeline actions and differentiation (#3182)
This commit is contained in:
@@ -696,6 +696,7 @@
|
|||||||
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; };
|
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; };
|
||||||
9C4EC28A921486B1775D7F8C /* IdentityConfirmedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */; };
|
9C4EC28A921486B1775D7F8C /* IdentityConfirmedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */; };
|
||||||
9C55746D8F6A3E35CFCF4A7A /* AuthenticationStartLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598F01EBD0C4CC550C644418 /* AuthenticationStartLogo.swift */; };
|
9C55746D8F6A3E35CFCF4A7A /* AuthenticationStartLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598F01EBD0C4CC550C644418 /* AuthenticationStartLogo.swift */; };
|
||||||
|
9CBB04365408F9D6F46BA3A7 /* PinnedEventsTimelineFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */; };
|
||||||
9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; };
|
9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; };
|
||||||
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
|
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
|
||||||
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
|
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
|
||||||
@@ -1869,6 +1870,7 @@
|
|||||||
A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
|
A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
|
||||||
A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
|
A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
A4A1003A0F7A1DFB47F4E2D0 /* TimelineItemMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMock.swift; sourceTree = "<group>"; };
|
A4A1003A0F7A1DFB47F4E2D0 /* TimelineItemMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMock.swift; sourceTree = "<group>"; };
|
||||||
|
A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = "<group>"; };
|
A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = "<group>"; };
|
||||||
A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCConfigurationProxy.swift; sourceTree = "<group>"; };
|
A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCConfigurationProxy.swift; sourceTree = "<group>"; };
|
||||||
A6B19D10B102956066AF117B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
A6B19D10B102956066AF117B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
||||||
@@ -3435,6 +3437,7 @@
|
|||||||
A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */,
|
A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */,
|
||||||
7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */,
|
7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */,
|
||||||
C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */,
|
C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */,
|
||||||
|
A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */,
|
||||||
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */,
|
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */,
|
||||||
0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */,
|
0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */,
|
||||||
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */,
|
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */,
|
||||||
@@ -6503,6 +6506,7 @@
|
|||||||
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */,
|
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */,
|
||||||
8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */,
|
8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */,
|
||||||
7E2BB42805C59DB57E95610F /* PillView.swift in Sources */,
|
7E2BB42805C59DB57E95610F /* PillView.swift in Sources */,
|
||||||
|
9CBB04365408F9D6F46BA3A7 /* PinnedEventsTimelineFlowCoordinator.swift in Sources */,
|
||||||
5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */,
|
5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */,
|
||||||
E184FFAD32342D3D6E2F89AA /* PinnedEventsTimelineScreenCoordinator.swift in Sources */,
|
E184FFAD32342D3D6E2F89AA /* PinnedEventsTimelineScreenCoordinator.swift in Sources */,
|
||||||
FEC03105D1BDE0F49BD7F243 /* PinnedEventsTimelineScreenModels.swift in Sources */,
|
FEC03105D1BDE0F49BD7F243 /* PinnedEventsTimelineScreenModels.swift in Sources */,
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
"action_tap_for_options" = "Tap for options";
|
"action_tap_for_options" = "Tap for options";
|
||||||
"action_try_again" = "Try again";
|
"action_try_again" = "Try again";
|
||||||
"action_unpin" = "Unpin";
|
"action_unpin" = "Unpin";
|
||||||
|
"action_view_in_timeline" = "View in timeline";
|
||||||
"action_view_source" = "View source";
|
"action_view_source" = "View source";
|
||||||
"action_yes" = "Yes";
|
"action_yes" = "Yes";
|
||||||
"action.load_more" = "Load more";
|
"action.load_more" = "Load more";
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 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 Foundation
|
||||||
|
|
||||||
|
enum PinnedEventsTimelineFlowCoordinatorAction {
|
||||||
|
case finished
|
||||||
|
case displayUser(userID: String)
|
||||||
|
case forwardedMessageToRoom(roomID: String)
|
||||||
|
case displayRoomScreenWithFocussedPin(eventID: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||||
|
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||||
|
private let userSession: UserSessionProtocol
|
||||||
|
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
|
||||||
|
private let roomProxy: JoinedRoomProxyProtocol
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
private let appMediator: AppMediatorProtocol
|
||||||
|
|
||||||
|
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineFlowCoordinatorAction, Never> = .init()
|
||||||
|
var actionsPublisher: AnyPublisher<PinnedEventsTimelineFlowCoordinatorAction, Never> {
|
||||||
|
actionsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(navigationStackCoordinator: NavigationStackCoordinator,
|
||||||
|
userSession: UserSessionProtocol,
|
||||||
|
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
||||||
|
roomProxy: JoinedRoomProxyProtocol,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
appMediator: AppMediatorProtocol) {
|
||||||
|
self.navigationStackCoordinator = navigationStackCoordinator
|
||||||
|
self.userSession = userSession
|
||||||
|
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
||||||
|
self.roomProxy = roomProxy
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
|
self.appMediator = appMediator
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
Task { await presentPinnedEventsTimeline() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearRoute(animated: Bool) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentPinnedEventsTimeline() async {
|
||||||
|
let userID = userSession.clientProxy.userID
|
||||||
|
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
|
||||||
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
|
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||||
|
|
||||||
|
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else {
|
||||||
|
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
|
||||||
|
timelineController: timelineController,
|
||||||
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
mediaPlayerProvider: MediaPlayerProvider(),
|
||||||
|
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||||
|
appMediator: appMediator))
|
||||||
|
|
||||||
|
coordinator.actions
|
||||||
|
.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
actionsSubject.send(.finished)
|
||||||
|
case .displayUser(let userID):
|
||||||
|
actionsSubject.send(.displayUser(userID: userID))
|
||||||
|
case .presentLocationViewer(let geoURI, let description):
|
||||||
|
presentMapNavigator(geoURI: geoURI, description: description)
|
||||||
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
|
presentMessageForwarding(with: forwardingItem)
|
||||||
|
case .displayRoomScreenWithFocussedPin(let eventID):
|
||||||
|
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentMapNavigator(geoURI: GeoURI, description: String?) {
|
||||||
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
|
let params = StaticLocationScreenCoordinatorParameters(interactionMode: .viewOnly(geoURI: geoURI, description: description), appMediator: appMediator)
|
||||||
|
let coordinator = StaticLocationScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
switch action {
|
||||||
|
case .selectedLocation:
|
||||||
|
// We don't handle the sending/picker case in this flow
|
||||||
|
break
|
||||||
|
case .close:
|
||||||
|
self.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
stackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentMessageForwarding(with forwardingItem: MessageForwardingItem) {
|
||||||
|
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
|
let parameters = MessageForwardingScreenCoordinatorParameters(forwardingItem: forwardingItem,
|
||||||
|
clientProxy: userSession.clientProxy,
|
||||||
|
roomSummaryProvider: roomSummaryProvider,
|
||||||
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
userIndicatorController: userIndicatorController)
|
||||||
|
let coordinator = MessageForwardingScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .sent(let roomID):
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
actionsSubject.send(.forwardedMessageToRoom(roomID: roomID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
stackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
// periphery:ignore - used to avoid deallocation
|
// periphery:ignore - used to avoid deallocation
|
||||||
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
|
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
|
||||||
// periphery:ignore - used to avoid deallocation
|
// periphery:ignore - used to avoid deallocation
|
||||||
|
private var pinnedEventsTimelineFlowCoordinator: PinnedEventsTimelineFlowCoordinator?
|
||||||
|
// periphery:ignore - used to avoid deallocation
|
||||||
private var childRoomFlowCoordinator: RoomFlowCoordinator?
|
private var childRoomFlowCoordinator: RoomFlowCoordinator?
|
||||||
|
|
||||||
private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
|
private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
|
||||||
@@ -166,7 +168,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else if roomID != roomProxy.id {
|
} else if roomID != roomProxy.id {
|
||||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||||
} else {
|
} else {
|
||||||
roomScreenCoordinator?.focusOnEvent(eventID: eventID)
|
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
||||||
}
|
}
|
||||||
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
|
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
|
||||||
break // These are converted to a room ID route one level above.
|
break // These are converted to a room ID route one level above.
|
||||||
@@ -194,7 +196,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch room {
|
switch room {
|
||||||
case .joined(let roomProxy):
|
case .joined(let roomProxy):
|
||||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||||
stateMachine.tryEvent(.presentRoom(focussedEventID: focussedEventID), userInfo: EventUserInfo(animated: animated))
|
let focussedEvent = focussedEventID.map { FocusEvent(eventID: $0, shouldSetPin: false) }
|
||||||
|
stateMachine.tryEvent(.presentRoom(focussedEvent: focussedEvent), userInfo: EventUserInfo(animated: animated))
|
||||||
default:
|
default:
|
||||||
stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
@@ -270,16 +273,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return .roomDetails(isRoot: false)
|
return .roomDetails(isRoot: false)
|
||||||
|
|
||||||
case (.room, .presentRoomMemberDetails(userID: let userID)):
|
case (.room, .presentRoomMemberDetails(userID: let userID)):
|
||||||
return .roomMemberDetails(userID: userID, fromRoomMembersList: false)
|
return .roomMemberDetails(userID: userID, previousState: .room)
|
||||||
case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)):
|
case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)):
|
||||||
return .roomMemberDetails(userID: userID, fromRoomMembersList: true)
|
return .roomMemberDetails(userID: userID, previousState: .roomMembersList)
|
||||||
case (.roomMemberDetails(_, let fromRoomMembersList), .dismissRoomMemberDetails):
|
case (.roomMemberDetails(_, let previousState), .dismissRoomMemberDetails):
|
||||||
return fromRoomMembersList ? .roomMembersList : .room
|
return previousState
|
||||||
|
|
||||||
case (.roomMemberDetails(_, fromRoomMembersList: false), .presentUserProfile(let userID)):
|
case (.roomMemberDetails(_, let previousState), .presentUserProfile(let userID)):
|
||||||
return .userProfile(userID: userID)
|
return .userProfile(userID: userID, previousState: previousState)
|
||||||
case (.userProfile, .dismissUserProfile):
|
case (.userProfile(_, let previousState), .dismissUserProfile):
|
||||||
return .room
|
return previousState
|
||||||
|
|
||||||
case (.roomDetails, .presentInviteUsersScreen):
|
case (.roomDetails, .presentInviteUsersScreen):
|
||||||
return .inviteUsersScreen(fromRoomMembersList: false)
|
return .inviteUsersScreen(fromRoomMembersList: false)
|
||||||
@@ -351,6 +354,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return .rolesAndPermissions
|
return .rolesAndPermissions
|
||||||
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen):
|
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen):
|
||||||
return .roomDetails(isRoot: false)
|
return .roomDetails(isRoot: false)
|
||||||
|
|
||||||
|
case (.roomDetails, .presentRoomMemberDetails(let userID)):
|
||||||
|
return .roomMemberDetails(userID: userID, previousState: fromState)
|
||||||
|
|
||||||
// Child flow
|
// Child flow
|
||||||
|
|
||||||
@@ -375,8 +381,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case (_, .dismissJoinRoomScreen, .complete):
|
case (_, .dismissJoinRoomScreen, .complete):
|
||||||
dismissFlow(animated: animated)
|
dismissFlow(animated: animated)
|
||||||
|
|
||||||
case (_, .presentRoom(let focussedEventID), .room):
|
case (_, .presentRoom(let focussedEvent), .room):
|
||||||
Task { await self.presentRoom(fromState: context.fromState, focussedEventID: focussedEventID, animated: animated) }
|
Task { await self.presentRoom(fromState: context.fromState, focussedEvent: focussedEvent, animated: animated) }
|
||||||
case (_, .dismissFlow, .complete):
|
case (_, .dismissFlow, .complete):
|
||||||
dismissFlow(animated: animated)
|
dismissFlow(animated: animated)
|
||||||
|
|
||||||
@@ -470,7 +476,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||||
Task { await self.presentPinnedEventsTimeline() }
|
presentPinnedEventsTimeline()
|
||||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
|
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -480,7 +486,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||||
Task { await self.presentPinnedEventsTimeline() }
|
presentPinnedEventsTimeline()
|
||||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
|
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -493,6 +499,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
presentRolesAndPermissionsScreen()
|
presentRolesAndPermissionsScreen()
|
||||||
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen, .roomDetails):
|
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen, .roomDetails):
|
||||||
rolesAndPermissionsFlowCoordinator = nil
|
rolesAndPermissionsFlowCoordinator = nil
|
||||||
|
|
||||||
|
case (.roomDetails, .presentRoomMemberDetails(let userID), .roomMemberDetails):
|
||||||
|
presentRoomMemberDetails(userID: userID)
|
||||||
|
case (.roomMemberDetails, .dismissRoomMemberDetails, .roomDetails):
|
||||||
|
break
|
||||||
|
|
||||||
|
case (.roomMemberDetails, .dismissUserProfile, .roomDetails):
|
||||||
|
break
|
||||||
|
|
||||||
// Child flow
|
// Child flow
|
||||||
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
|
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
|
||||||
@@ -525,9 +539,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
/// Updates the navigation stack so it displays the timeline for the given room
|
/// Updates the navigation stack so it displays the timeline for the given room
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - fromState: The state that asked for the room presentation.
|
/// - fromState: The state that asked for the room presentation.
|
||||||
/// - focussedEventID: An (optional) event ID that the timeline should be focussed around.
|
/// - focussedEvent: An (optional) struct that contains the event ID that the timeline should be focussed around, and a boolean telling if such event should update the pinned events banner
|
||||||
/// - animated: whether it should animate the transition
|
/// - animated: whether it should animate the transition
|
||||||
private func presentRoom(fromState: State, focussedEventID: String? = nil, animated: Bool) async {
|
private func presentRoom(fromState: State, focussedEvent: FocusEvent?, animated: Bool) async {
|
||||||
// If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine
|
// If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine
|
||||||
// through the correct states before presenting the room
|
// through the correct states before presenting the room
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
@@ -545,8 +559,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
// The room is already on the stack, no need to present it again
|
// The room is already on the stack, no need to present it again
|
||||||
|
|
||||||
// Check if we need to focus on an event
|
// Check if we need to focus on an event
|
||||||
if let focussedEventID {
|
if let focussedEvent {
|
||||||
roomScreenCoordinator?.focusOnEvent(eventID: focussedEventID)
|
roomScreenCoordinator?.focusOnEvent(focussedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -565,7 +579,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||||
|
|
||||||
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
|
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
|
||||||
initialFocussedEventID: focussedEventID,
|
initialFocussedEventID: focussedEvent?.eventID,
|
||||||
timelineItemFactory: timelineItemFactory)
|
timelineItemFactory: timelineItemFactory)
|
||||||
self.timelineController = timelineController
|
self.timelineController = timelineController
|
||||||
|
|
||||||
@@ -576,7 +590,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
|
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
|
||||||
|
|
||||||
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
|
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
|
||||||
focussedEventID: focussedEventID,
|
focussedEvent: focussedEvent,
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
mediaPlayerProvider: MediaPlayerProvider(),
|
mediaPlayerProvider: MediaPlayerProvider(),
|
||||||
@@ -660,7 +674,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) {
|
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) {
|
||||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||||
stateMachine.tryEvent(.presentRoom(focussedEventID: nil), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated))
|
||||||
|
|
||||||
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount))
|
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount))
|
||||||
} else {
|
} else {
|
||||||
@@ -964,42 +978,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentPinnedEventsTimeline() async {
|
|
||||||
let userID = userSession.clientProxy.userID
|
|
||||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
|
|
||||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
|
||||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
|
||||||
|
|
||||||
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else {
|
|
||||||
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
|
||||||
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
|
|
||||||
timelineController: timelineController,
|
|
||||||
mediaProvider: userSession.mediaProvider,
|
|
||||||
mediaPlayerProvider: MediaPlayerProvider(),
|
|
||||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
|
||||||
appMediator: appMediator))
|
|
||||||
|
|
||||||
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) {
|
private func presentPollForm(mode: PollFormMode) {
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode))
|
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode))
|
||||||
@@ -1345,6 +1323,43 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
coordinator.start()
|
coordinator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presentPinnedEventsTimeline() {
|
||||||
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
let coordinator = PinnedEventsTimelineFlowCoordinator(navigationStackCoordinator: stackCoordinator,
|
||||||
|
userSession: userSession,
|
||||||
|
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||||
|
roomProxy: roomProxy,
|
||||||
|
userIndicatorController: userIndicatorController,
|
||||||
|
appMediator: appMediator)
|
||||||
|
|
||||||
|
coordinator.actionsPublisher.sink { [weak self] action in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .finished:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .displayUser(let userID):
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
|
||||||
|
case .forwardedMessageToRoom(let roomID):
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
|
||||||
|
case .displayRoomScreenWithFocussedPin(let eventID):
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
stateMachine.tryEvent(.presentRoom(focussedEvent: .init(eventID: eventID, shouldSetPin: true)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
pinnedEventsTimelineFlowCoordinator = coordinator
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||||
|
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
|
||||||
|
}
|
||||||
|
coordinator.start()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Child Flow
|
// MARK: - Child Flow
|
||||||
|
|
||||||
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
|
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
|
||||||
@@ -1405,8 +1420,8 @@ private extension RoomFlowCoordinator {
|
|||||||
case notificationSettings
|
case notificationSettings
|
||||||
case globalNotificationSettings
|
case globalNotificationSettings
|
||||||
case roomMembersList
|
case roomMembersList
|
||||||
case roomMemberDetails(userID: String, fromRoomMembersList: Bool)
|
case roomMemberDetails(userID: String, previousState: State)
|
||||||
case userProfile(userID: String)
|
case userProfile(userID: String, previousState: State)
|
||||||
case inviteUsersScreen(fromRoomMembersList: Bool)
|
case inviteUsersScreen(fromRoomMembersList: Bool)
|
||||||
case mediaUploadPicker(source: MediaPickerScreenSource)
|
case mediaUploadPicker(source: MediaPickerScreenSource)
|
||||||
case mediaUploadPreview(fileURL: URL)
|
case mediaUploadPreview(fileURL: URL)
|
||||||
@@ -1434,7 +1449,7 @@ private extension RoomFlowCoordinator {
|
|||||||
case presentJoinRoomScreen(via: [String])
|
case presentJoinRoomScreen(via: [String])
|
||||||
case dismissJoinRoomScreen
|
case dismissJoinRoomScreen
|
||||||
|
|
||||||
case presentRoom(focussedEventID: String?)
|
case presentRoom(focussedEvent: FocusEvent?)
|
||||||
case dismissFlow
|
case dismissFlow
|
||||||
|
|
||||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
@@ -1497,12 +1512,6 @@ private extension RoomFlowCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension GeoURI {
|
|
||||||
var bodyMessage: String {
|
|
||||||
"Location was shared at \(string)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Result {
|
private extension Result {
|
||||||
var isFailure: Bool {
|
var isFailure: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -1518,3 +1527,10 @@ private enum PinnedEventsTimelineSource: Hashable {
|
|||||||
case room
|
case room
|
||||||
case details(isRoot: Bool)
|
case details(isRoot: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FocusEvent: Hashable {
|
||||||
|
/// The event ID that the timeline should be focussed around
|
||||||
|
let eventID: String
|
||||||
|
/// if the focus is coming from the pinned timeline, this should also update the pin banner
|
||||||
|
let shouldSetPin: Bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ internal enum L10n {
|
|||||||
internal static var actionTryAgain: String { return L10n.tr("Localizable", "action_try_again") }
|
internal static var actionTryAgain: String { return L10n.tr("Localizable", "action_try_again") }
|
||||||
/// Unpin
|
/// Unpin
|
||||||
internal static var actionUnpin: String { return L10n.tr("Localizable", "action_unpin") }
|
internal static var actionUnpin: String { return L10n.tr("Localizable", "action_unpin") }
|
||||||
|
/// View in timeline
|
||||||
|
internal static var actionViewInTimeline: String { return L10n.tr("Localizable", "action_view_in_timeline") }
|
||||||
/// View source
|
/// View source
|
||||||
internal static var actionViewSource: String { return L10n.tr("Localizable", "action_view_source") }
|
internal static var actionViewSource: String { return L10n.tr("Localizable", "action_view_source") }
|
||||||
/// Yes
|
/// Yes
|
||||||
|
|||||||
+26
-2
@@ -28,8 +28,10 @@ struct PinnedEventsTimelineScreenCoordinatorParameters {
|
|||||||
|
|
||||||
enum PinnedEventsTimelineScreenCoordinatorAction {
|
enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||||
case dismiss
|
case dismiss
|
||||||
|
case displayUser(userID: String)
|
||||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
case presentLocationViewer(geoURI: GeoURI, description: String?)
|
||||||
|
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||||
|
case displayRoomScreenWithFocussedPin(eventID: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||||
@@ -50,6 +52,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel = PinnedEventsTimelineScreenViewModel()
|
viewModel = PinnedEventsTimelineScreenViewModel()
|
||||||
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||||
timelineController: parameters.timelineController,
|
timelineController: parameters.timelineController,
|
||||||
|
isPinnedEventsTimeline: true,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
||||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||||
@@ -70,6 +73,27 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
timelineViewModel.actions.sink { [weak self] action in
|
||||||
|
MXLog.info("Coordinator: received timeline view model action: \(action)")
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .tappedOnSenderDetails(let userID):
|
||||||
|
actionsSubject.send(.displayUser(userID: userID))
|
||||||
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
|
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
||||||
|
case .displayLocation(_, let geoURI, let description):
|
||||||
|
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
||||||
|
case .viewInRoomTimeline(let eventID):
|
||||||
|
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||||
|
// These other actions will not be handled in this view
|
||||||
|
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .composer, .hasScrolled:
|
||||||
|
// These actions are not handled in this coordinator
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
|
|||||||
+18
-1
@@ -35,8 +35,24 @@ struct PinnedEventsTimelineScreen: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
.background(.compound.bgCanvasDefault)
|
.background(.compound.bgCanvasDefault)
|
||||||
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
|
||||||
.interactiveDismissDisabled()
|
.interactiveDismissDisabled()
|
||||||
|
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
||||||
|
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
|
||||||
|
.sheet(item: $timelineContext.actionMenuInfo) { info in
|
||||||
|
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
|
||||||
|
canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf,
|
||||||
|
canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers,
|
||||||
|
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
|
||||||
|
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
|
||||||
|
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
||||||
|
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
|
||||||
|
isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline)
|
||||||
|
.makeActions()
|
||||||
|
if let actions {
|
||||||
|
TimelineItemMenu(item: info.item, actions: actions)
|
||||||
|
.environmentObject(timelineContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -83,6 +99,7 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
timelineController.timelineItems = []
|
timelineController.timelineItems = []
|
||||||
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
|
isPinnedEventsTimeline: true,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ struct RoomPollsHistoryScreen: View {
|
|||||||
Text(DateFormatter.pollTimestamp.string(from: pollTimelineItem.timestamp))
|
Text(DateFormatter.pollTimestamp.string(from: pollTimelineItem.timestamp))
|
||||||
.font(.compound.bodySM)
|
.font(.compound.bodySM)
|
||||||
.foregroundColor(.compound.textSecondary)
|
.foregroundColor(.compound.textSecondary)
|
||||||
PollView(poll: pollTimelineItem.item.poll, editable: pollTimelineItem.item.isEditable) { action in
|
PollView(poll: pollTimelineItem.item.poll,
|
||||||
|
state: .full(isEditable: pollTimelineItem.item.isEditable)) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .selectOption(let optionID):
|
case .selectOption(let optionID):
|
||||||
guard let pollStartID = pollTimelineItem.item.id.eventID else { return }
|
guard let pollStartID = pollTimelineItem.item.id.eventID else { return }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import WysiwygComposer
|
|||||||
|
|
||||||
struct RoomScreenCoordinatorParameters {
|
struct RoomScreenCoordinatorParameters {
|
||||||
let roomProxy: JoinedRoomProxyProtocol
|
let roomProxy: JoinedRoomProxyProtocol
|
||||||
var focussedEventID: String?
|
var focussedEvent: FocusEvent?
|
||||||
let timelineController: RoomTimelineControllerProtocol
|
let timelineController: RoomTimelineControllerProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||||
@@ -63,7 +63,13 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(parameters: RoomScreenCoordinatorParameters) {
|
init(parameters: RoomScreenCoordinatorParameters) {
|
||||||
|
var selectedPinnedEventID: String?
|
||||||
|
if let focussedEvent = parameters.focussedEvent {
|
||||||
|
selectedPinnedEventID = focussedEvent.shouldSetPin ? focussedEvent.eventID : nil
|
||||||
|
}
|
||||||
|
|
||||||
roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
|
roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
|
||||||
|
initialSelectedPinnedEventID: selectedPinnedEventID,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher,
|
ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher,
|
||||||
appMediator: parameters.appMediator,
|
appMediator: parameters.appMediator,
|
||||||
@@ -71,8 +77,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
analyticsService: ServiceLocator.shared.analytics)
|
analyticsService: ServiceLocator.shared.analytics)
|
||||||
|
|
||||||
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||||
focussedEventID: parameters.focussedEventID,
|
focussedEventID: parameters.focussedEvent?.eventID,
|
||||||
timelineController: parameters.timelineController,
|
timelineController: parameters.timelineController,
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
||||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||||
@@ -133,6 +140,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
composerViewModel.process(timelineAction: action)
|
composerViewModel.process(timelineAction: action)
|
||||||
case .hasScrolled(direction: let direction):
|
case .hasScrolled(direction: let direction):
|
||||||
roomViewModel.timelineHasScrolled(direction: direction)
|
roomViewModel.timelineHasScrolled(direction: direction)
|
||||||
|
case .viewInRoomTimeline:
|
||||||
|
fatalError("The action: \(action) should not be sent to this coordinator")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -151,7 +160,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
switch actions {
|
switch actions {
|
||||||
case .focusEvent(eventID: let eventID):
|
case .focusEvent(eventID: let eventID):
|
||||||
focusOnEvent(eventID: eventID)
|
focusOnEvent(FocusEvent(eventID: eventID, shouldSetPin: false))
|
||||||
case .displayPinnedEventsTimeline:
|
case .displayPinnedEventsTimeline:
|
||||||
actionsSubject.send(.presentPinnedEventsTimeline)
|
actionsSubject.send(.presentPinnedEventsTimeline)
|
||||||
case .displayRoomDetails:
|
case .displayRoomDetails:
|
||||||
@@ -168,7 +177,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
composerViewModel.loadDraft()
|
composerViewModel.loadDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
func focusOnEvent(eventID: String) {
|
func focusOnEvent(_ focussedEvent: FocusEvent) {
|
||||||
|
let eventID = focussedEvent.eventID
|
||||||
|
if focussedEvent.shouldSetPin {
|
||||||
|
roomViewModel.setSelectedPinnedEventID(eventID)
|
||||||
|
}
|
||||||
Task { await timelineViewModel.focusOnEvent(eventID: eventID) }
|
Task { await timelineViewModel.focusOnEvent(eventID: eventID) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ enum PinnedEventsBannerState: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedPinEventID: String? {
|
var selectedPinnedEventID: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .loaded(let state):
|
case .loaded(let state):
|
||||||
return state.selectedPinEventID
|
return state.selectedPinnedEventID
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -93,10 +93,10 @@ enum PinnedEventsBannerState: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedPinIndex: Int {
|
var selectedPinnedIndex: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case .loaded(let state):
|
case .loaded(let state):
|
||||||
return state.selectedPinIndex
|
return state.selectedPinnedIndex
|
||||||
case .loading(let numbersOfEvents):
|
case .loading(let numbersOfEvents):
|
||||||
// We always want the index to be the last one when loading, since is the default one.
|
// We always want the index to be the last one when loading, since is the default one.
|
||||||
return numbersOfEvents - 1
|
return numbersOfEvents - 1
|
||||||
@@ -108,12 +108,12 @@ enum PinnedEventsBannerState: Equatable {
|
|||||||
case .loading:
|
case .loading:
|
||||||
return AttributedString(L10n.screenRoomPinnedBannerLoadingDescription)
|
return AttributedString(L10n.screenRoomPinnedBannerLoadingDescription)
|
||||||
case .loaded(let state):
|
case .loaded(let state):
|
||||||
return state.selectedPinContent
|
return state.selectedPinnedContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var bannerIndicatorDescription: AttributedString {
|
var bannerIndicatorDescription: AttributedString {
|
||||||
let index = selectedPinIndex + 1
|
let index = selectedPinnedIndex + 1
|
||||||
let boldPlaceholder = "{bold}"
|
let boldPlaceholder = "{bold}"
|
||||||
var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder))
|
var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder))
|
||||||
var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, count))
|
var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, count))
|
||||||
@@ -136,10 +136,72 @@ enum PinnedEventsBannerState: Equatable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .loading:
|
case .loading:
|
||||||
// The default selected event should always be the last one.
|
// The default selected event should always be the last one.
|
||||||
self = .loaded(state: .init(pinnedEventContents: pinnedEventContents, selectedPinEventID: pinnedEventContents.keys.last))
|
self = .loaded(state: .init(pinnedEventContents: pinnedEventContents, selectedPinnedEventID: pinnedEventContents.keys.last))
|
||||||
case .loaded(var state):
|
case .loaded(var state):
|
||||||
state.pinnedEventContents = pinnedEventContents
|
state.pinnedEventContents = pinnedEventContents
|
||||||
self = .loaded(state: state)
|
self = .loaded(state: state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that if we are setting this value, this is definitely sent from the pinned events timeline
|
||||||
|
// so we can assume that the pinned events timeline is already loaded and we only need to set the
|
||||||
|
// selection for the loaded state
|
||||||
|
mutating func setSelectedPinnedEventID(_ eventID: String) {
|
||||||
|
switch self {
|
||||||
|
case .loaded(var state):
|
||||||
|
state.selectedPinnedEventID = eventID
|
||||||
|
self = .loaded(state: state)
|
||||||
|
case .loading:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PinnedEventsState: Equatable {
|
||||||
|
var pinnedEventContents: OrderedDictionary<String, AttributedString> = [:] {
|
||||||
|
didSet {
|
||||||
|
if selectedPinnedEventID == nil, !pinnedEventContents.keys.isEmpty {
|
||||||
|
// The default selected event should always be the last one.
|
||||||
|
selectedPinnedEventID = pinnedEventContents.keys.last
|
||||||
|
} else if pinnedEventContents.isEmpty {
|
||||||
|
selectedPinnedEventID = nil
|
||||||
|
} else if let selectedPinnedEventID, !pinnedEventContents.keys.set.contains(selectedPinnedEventID) {
|
||||||
|
self.selectedPinnedEventID = pinnedEventContents.keys.last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedPinnedEventID: String?
|
||||||
|
|
||||||
|
var selectedPinnedIndex: Int {
|
||||||
|
let defaultValue = pinnedEventContents.isEmpty ? 0 : pinnedEventContents.count - 1
|
||||||
|
guard let selectedPinnedEventID else {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return pinnedEventContents.keys.firstIndex(of: selectedPinnedEventID) ?? defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedPinnedContent: AttributedString {
|
||||||
|
var content = AttributedString(" ")
|
||||||
|
if let selectedPinnedEventID,
|
||||||
|
let pinnedEventContent = pinnedEventContents[selectedPinnedEventID] {
|
||||||
|
content = pinnedEventContent
|
||||||
|
}
|
||||||
|
content.font = .compound.bodyMD
|
||||||
|
content.link = nil
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func previousPin() {
|
||||||
|
guard !pinnedEventContents.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let currentIndex = selectedPinnedIndex
|
||||||
|
let nextIndex = currentIndex - 1
|
||||||
|
if nextIndex == -1 {
|
||||||
|
selectedPinnedEventID = pinnedEventContents.keys.last
|
||||||
|
} else {
|
||||||
|
selectedPinnedEventID = pinnedEventContents.keys[nextIndex % pinnedEventContents.count]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
private let analyticsService: AnalyticsService
|
private let analyticsService: AnalyticsService
|
||||||
private let pinnedEventStringBuilder: RoomEventStringBuilder
|
private let pinnedEventStringBuilder: RoomEventStringBuilder
|
||||||
|
private var initialSelectedPinnedEventID: String?
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<RoomScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<RoomScreenViewModelAction, Never> = .init()
|
||||||
var actions: AnyPublisher<RoomScreenViewModelAction, Never> {
|
var actions: AnyPublisher<RoomScreenViewModelAction, Never> {
|
||||||
@@ -52,6 +53,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(roomProxy: JoinedRoomProxyProtocol,
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
|
initialSelectedPinnedEventID: String?,
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>,
|
ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>,
|
||||||
appMediator: AppMediatorProtocol,
|
appMediator: AppMediatorProtocol,
|
||||||
@@ -61,6 +63,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
self.appMediator = appMediator
|
self.appMediator = appMediator
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
self.analyticsService = analyticsService
|
self.analyticsService = analyticsService
|
||||||
|
self.initialSelectedPinnedEventID = initialSelectedPinnedEventID
|
||||||
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
|
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
|
||||||
|
|
||||||
super.init(initialViewState: .init(roomTitle: roomProxy.roomTitle,
|
super.init(initialViewState: .init(roomTitle: roomProxy.roomTitle,
|
||||||
@@ -69,13 +72,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
bindings: .init()),
|
bindings: .init()),
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await handleRoomInfoUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
|
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func process(viewAction: RoomScreenViewAction) {
|
override func process(viewAction: RoomScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .tappedPinnedEventsBanner:
|
case .tappedPinnedEventsBanner:
|
||||||
if let eventID = state.pinnedEventsBannerState.selectedPinEventID {
|
if let eventID = state.pinnedEventsBannerState.selectedPinnedEventID {
|
||||||
actionsSubject.send(.focusEvent(eventID: eventID))
|
actionsSubject.send(.focusEvent(eventID: eventID))
|
||||||
}
|
}
|
||||||
state.pinnedEventsBannerState.previousPin()
|
state.pinnedEventsBannerState.previousPin()
|
||||||
@@ -94,6 +101,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.lastScrollDirection = direction
|
state.lastScrollDirection = direction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSelectedPinnedEventID(_ eventID: String) {
|
||||||
|
state.pinnedEventsBannerState.setSelectedPinnedEventID(eventID)
|
||||||
|
}
|
||||||
|
|
||||||
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
||||||
let roomInfoSubscription = roomProxy
|
let roomInfoSubscription = roomProxy
|
||||||
.actionsPublisher
|
.actionsPublisher
|
||||||
@@ -110,13 +121,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
// Don't guard let self here, otherwise the for await will strongify the self reference creating a strong reference cycle.
|
|
||||||
// If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update.
|
|
||||||
await self?.handleRoomInfoUpdate()
|
|
||||||
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values {
|
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values {
|
||||||
guard !Task.isCancelled else {
|
guard !Task.isCancelled else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await self?.handleRoomInfoUpdate()
|
await self?.handleRoomInfoUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +168,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.pinnedEventsBannerState.setPinnedEventContents(pinnedEventContents)
|
state.pinnedEventsBannerState.setPinnedEventContents(pinnedEventContents)
|
||||||
|
|
||||||
|
// If it's the first time we are setting the pinned events, we should select the initial event if available.
|
||||||
|
if let initialSelectedPinnedEventID {
|
||||||
|
state.pinnedEventsBannerState.setSelectedPinnedEventID(initialSelectedPinnedEventID)
|
||||||
|
self.initialSelectedPinnedEventID = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleRoomInfoUpdate() async {
|
private func handleRoomInfoUpdate() async {
|
||||||
@@ -194,6 +209,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
extension RoomScreenViewModel {
|
extension RoomScreenViewModel {
|
||||||
static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel {
|
static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel {
|
||||||
RoomScreenViewModel(roomProxy: roomProxyMock,
|
RoomScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
appMediator: AppMediatorMock.default,
|
appMediator: AppMediatorMock.default,
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ protocol RoomScreenViewModelProtocol {
|
|||||||
var context: RoomScreenViewModel.Context { get }
|
var context: RoomScreenViewModel.Context { get }
|
||||||
|
|
||||||
func timelineHasScrolled(direction: ScrollDirection)
|
func timelineHasScrolled(direction: ScrollDirection)
|
||||||
|
func setSelectedPinnedEventID(_ eventID: String)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -38,7 +38,7 @@ struct PinnedItemsBannerView: View {
|
|||||||
Button { onMainButtonTap() } label: {
|
Button { onMainButtonTap() } label: {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
PinnedItemsIndicatorView(pinIndex: state.selectedPinIndex, pinsCount: state.count)
|
PinnedItemsIndicatorView(pinIndex: state.selectedPinnedIndex, pinsCount: state.count)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD)
|
CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD)
|
||||||
.foregroundColor(Color.compound.iconSecondaryAlpha)
|
.foregroundColor(Color.compound.iconSecondaryAlpha)
|
||||||
@@ -98,16 +98,16 @@ struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview {
|
|||||||
PinnedItemsBannerView(state: .loaded(state: .init(pinnedEventContents: ["1": "Content",
|
PinnedItemsBannerView(state: .loaded(state: .init(pinnedEventContents: ["1": "Content",
|
||||||
"2": "2",
|
"2": "2",
|
||||||
"3": "3"],
|
"3": "3"],
|
||||||
selectedPinEventID: "1")),
|
selectedPinnedEventID: "1")),
|
||||||
onMainButtonTap: { },
|
onMainButtonTap: { },
|
||||||
onViewAllButtonTap: { })
|
onViewAllButtonTap: { })
|
||||||
PinnedItemsBannerView(state: .loaded(state: .init(pinnedEventContents: ["1": "Very very very very long content here",
|
PinnedItemsBannerView(state: .loaded(state: .init(pinnedEventContents: ["1": "Very very very very long content here",
|
||||||
"2": "2"],
|
"2": "2"],
|
||||||
selectedPinEventID: "1")),
|
selectedPinnedEventID: "1")),
|
||||||
onMainButtonTap: { },
|
onMainButtonTap: { },
|
||||||
onViewAllButtonTap: { })
|
onViewAllButtonTap: { })
|
||||||
PinnedItemsBannerView(state: .loaded(state: .init(pinnedEventContents: ["1": attributedContent],
|
PinnedItemsBannerView(state: .loaded(state: .init(pinnedEventContents: ["1": attributedContent],
|
||||||
selectedPinEventID: "1")),
|
selectedPinnedEventID: "1")),
|
||||||
onMainButtonTap: { },
|
onMainButtonTap: { },
|
||||||
onViewAllButtonTap: { })
|
onViewAllButtonTap: { })
|
||||||
PinnedItemsBannerView(state: .loading(numbersOfEvents: 5),
|
PinnedItemsBannerView(state: .loading(numbersOfEvents: 5),
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ struct RoomScreen: View {
|
|||||||
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
|
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
|
||||||
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
|
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
|
||||||
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
||||||
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled).makeActions()
|
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
|
||||||
|
isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline)
|
||||||
|
.makeActions()
|
||||||
if let actions {
|
if let actions {
|
||||||
TimelineItemMenu(item: info.item, actions: actions)
|
TimelineItemMenu(item: info.item, actions: actions)
|
||||||
.environmentObject(timelineContext)
|
.environmentObject(timelineContext)
|
||||||
@@ -218,6 +220,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
|
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
|
||||||
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ enum TimelineInteractionHandlerAction {
|
|||||||
|
|
||||||
case displayAudioRecorderPermissionError
|
case displayAudioRecorderPermissionError
|
||||||
case displayErrorToast(String)
|
case displayErrorToast(String)
|
||||||
|
|
||||||
|
case viewInRoomTimeline(eventID: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -176,6 +178,9 @@ class TimelineInteractionHandler {
|
|||||||
case .unpin:
|
case .unpin:
|
||||||
guard let eventID = itemID.eventID else { return }
|
guard let eventID = itemID.eventID else { return }
|
||||||
Task { await timelineController.unpin(eventID: eventID) }
|
Task { await timelineController.unpin(eventID: eventID) }
|
||||||
|
case .viewInRoomTimeline:
|
||||||
|
guard let eventID = itemID.eventID else { return }
|
||||||
|
actionsSubject.send(.viewInRoomTimeline(eventID: eventID))
|
||||||
}
|
}
|
||||||
|
|
||||||
if action.switchToDefaultComposer {
|
if action.switchToDefaultComposer {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ enum TimelineViewModelAction {
|
|||||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||||
case composer(action: TimelineComposerAction)
|
case composer(action: TimelineComposerAction)
|
||||||
case hasScrolled(direction: ScrollDirection)
|
case hasScrolled(direction: ScrollDirection)
|
||||||
|
case viewInRoomTimeline(eventID: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineViewPollAction {
|
enum TimelineViewPollAction {
|
||||||
@@ -89,6 +90,7 @@ enum TimelineComposerAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineViewState: BindableState {
|
struct TimelineViewState: BindableState {
|
||||||
|
let isPinnedEventsTimeline: Bool
|
||||||
var roomID: String
|
var roomID: String
|
||||||
var members: [String: RoomMemberState] = [:]
|
var members: [String: RoomMemberState] = [:]
|
||||||
var typingMembers: [String] = []
|
var typingMembers: [String] = []
|
||||||
@@ -222,52 +224,3 @@ enum ScrollDirection: Equatable {
|
|||||||
case top
|
case top
|
||||||
case bottom
|
case bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PinnedEventsState: Equatable {
|
|
||||||
var pinnedEventContents: OrderedDictionary<String, AttributedString> = [:] {
|
|
||||||
didSet {
|
|
||||||
if selectedPinEventID == nil, !pinnedEventContents.keys.isEmpty {
|
|
||||||
// The default selected event should always be the last one.
|
|
||||||
selectedPinEventID = pinnedEventContents.keys.last
|
|
||||||
} else if pinnedEventContents.isEmpty {
|
|
||||||
selectedPinEventID = nil
|
|
||||||
} else if let selectedPinEventID, !pinnedEventContents.keys.set.contains(selectedPinEventID) {
|
|
||||||
self.selectedPinEventID = pinnedEventContents.keys.last
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private(set) var selectedPinEventID: String?
|
|
||||||
|
|
||||||
var selectedPinIndex: Int {
|
|
||||||
let defaultValue = pinnedEventContents.isEmpty ? 0 : pinnedEventContents.count - 1
|
|
||||||
guard let selectedPinEventID else {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return pinnedEventContents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedPinContent: AttributedString {
|
|
||||||
var content = AttributedString(" ")
|
|
||||||
if let selectedPinEventID,
|
|
||||||
let pinnedEventContent = pinnedEventContents[selectedPinEventID] {
|
|
||||||
content = pinnedEventContent
|
|
||||||
}
|
|
||||||
content.font = .compound.bodyMD
|
|
||||||
content.link = nil
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
mutating func previousPin() {
|
|
||||||
guard !pinnedEventContents.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let currentIndex = selectedPinIndex
|
|
||||||
let nextIndex = currentIndex - 1
|
|
||||||
if nextIndex == -1 {
|
|
||||||
selectedPinEventID = pinnedEventContents.keys.last
|
|
||||||
} else {
|
|
||||||
selectedPinEventID = pinnedEventContents.keys[nextIndex % pinnedEventContents.count]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
init(roomProxy: JoinedRoomProxyProtocol,
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
focussedEventID: String? = nil,
|
focussedEventID: String? = nil,
|
||||||
timelineController: RoomTimelineControllerProtocol,
|
timelineController: RoomTimelineControllerProtocol,
|
||||||
|
isPinnedEventsTimeline: Bool,
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
mediaPlayerProvider: MediaPlayerProviderProtocol,
|
mediaPlayerProvider: MediaPlayerProviderProtocol,
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerProtocol,
|
voiceMessageMediaManager: VoiceMessageMediaManagerProtocol,
|
||||||
@@ -80,7 +81,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
analyticsService: analyticsService)
|
analyticsService: analyticsService)
|
||||||
|
|
||||||
super.init(initialViewState: TimelineViewState(roomID: roomProxy.id,
|
super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: isPinnedEventsTimeline,
|
||||||
|
roomID: roomProxy.id,
|
||||||
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
||||||
timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
|
timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
|
||||||
ownUserID: roomProxy.ownUserID,
|
ownUserID: roomProxy.ownUserID,
|
||||||
@@ -93,6 +95,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
showFocusLoadingIndicator()
|
showFocusLoadingIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await updatePinnedEventIDs()
|
||||||
|
}
|
||||||
|
|
||||||
setupSubscriptions()
|
setupSubscriptions()
|
||||||
setupDirectRoomSubscriptionsIfNeeded()
|
setupDirectRoomSubscriptionsIfNeeded()
|
||||||
|
|
||||||
@@ -375,9 +381,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
.actionsPublisher
|
.actionsPublisher
|
||||||
.filter { $0 == .roomInfoUpdate }
|
.filter { $0 == .roomInfoUpdate }
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
// Don't guard let self here, otherwise the for await will strongify the self reference creating a strong reference cycle.
|
|
||||||
// If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update.
|
|
||||||
await self?.updatePinnedEventIDs()
|
|
||||||
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values {
|
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values {
|
||||||
guard !Task.isCancelled else {
|
guard !Task.isCancelled else {
|
||||||
return
|
return
|
||||||
@@ -429,6 +432,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
}
|
}
|
||||||
case .showDebugInfo(let debugInfo):
|
case .showDebugInfo(let debugInfo):
|
||||||
state.bindings.debugInfo = debugInfo
|
state.bindings.debugInfo = debugInfo
|
||||||
|
case .viewInRoomTimeline(let eventID):
|
||||||
|
actionsSubject.send(.viewInRoomTimeline(eventID: eventID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -641,13 +646,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
} else {
|
} else {
|
||||||
for (index, item) in itemGroup.enumerated() {
|
for (index, item) in itemGroup.enumerated() {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .first),
|
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .first),
|
||||||
forKey: item.id.timelineID)
|
forKey: item.id.timelineID)
|
||||||
} else if index == itemGroup.count - 1 {
|
} else if index == itemGroup.count - 1 {
|
||||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .last),
|
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .last),
|
||||||
forKey: item.id.timelineID)
|
forKey: item.id.timelineID)
|
||||||
} else {
|
} else {
|
||||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .middle),
|
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .middle),
|
||||||
forKey: item.id.timelineID)
|
forKey: item.id.timelineID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,6 +827,7 @@ extension TimelineViewModel {
|
|||||||
static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||||
focussedEventID: nil,
|
focussedEventID: nil,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -829,6 +835,18 @@ extension TimelineViewModel {
|
|||||||
appMediator: AppMediatorMock.default,
|
appMediator: AppMediatorMock.default,
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics)
|
analyticsService: ServiceLocator.shared.analytics)
|
||||||
|
|
||||||
|
static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||||
|
focussedEventID: nil,
|
||||||
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: true,
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analyticsService: ServiceLocator.shared.analytics)
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct TimelineContextKey: EnvironmentKey {
|
private struct TimelineContextKey: EnvironmentKey {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
|||||||
case endPoll(pollStartID: String)
|
case endPoll(pollStartID: String)
|
||||||
case pin
|
case pin
|
||||||
case unpin
|
case unpin
|
||||||
|
case viewInRoomTimeline
|
||||||
|
|
||||||
var id: Self { self }
|
var id: Self { self }
|
||||||
|
|
||||||
@@ -106,6 +107,15 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var canAppearInPinnedEventsTimeline: Bool {
|
||||||
|
switch self {
|
||||||
|
case .viewInRoomTimeline, .pin, .unpin, .forward, .redact:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The action's label.
|
/// The action's label.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var label: some View {
|
var label: some View {
|
||||||
@@ -139,6 +149,8 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
|||||||
Label(L10n.actionPin, icon: \.pin)
|
Label(L10n.actionPin, icon: \.pin)
|
||||||
case .unpin:
|
case .unpin:
|
||||||
Label(L10n.actionUnpin, icon: \.unpin)
|
Label(L10n.actionUnpin, icon: \.unpin)
|
||||||
|
case .viewInRoomTimeline:
|
||||||
|
Label(L10n.actionViewInTimeline, icon: \.visibilityOn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -24,6 +24,7 @@ struct TimelineItemMenuActionProvider {
|
|||||||
let pinnedEventIDs: Set<String>
|
let pinnedEventIDs: Set<String>
|
||||||
let isDM: Bool
|
let isDM: Bool
|
||||||
let isViewSourceEnabled: Bool
|
let isViewSourceEnabled: Bool
|
||||||
|
let isPinnedEventsTimeline: Bool
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
func makeActions() -> TimelineItemMenuActions? {
|
func makeActions() -> TimelineItemMenuActions? {
|
||||||
@@ -102,8 +103,13 @@ struct TimelineItemMenuActionProvider {
|
|||||||
if item.isRedacted {
|
if item.isRedacted {
|
||||||
actions = actions.filter(\.canAppearInRedacted)
|
actions = actions.filter(\.canAppearInRedacted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPinnedEventsTimeline {
|
||||||
|
actions.insert(.viewInRoomTimeline, at: 0)
|
||||||
|
actions = actions.filter(\.canAppearInPinnedEventsTimeline)
|
||||||
|
}
|
||||||
|
|
||||||
return .init(isReactable: item.isReactable, actions: actions, debugActions: debugActions)
|
return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable, actions: actions, debugActions: debugActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool {
|
private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool {
|
||||||
|
|||||||
@@ -23,21 +23,48 @@ enum PollViewAction {
|
|||||||
case end
|
case end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PollViewState {
|
||||||
|
case preview
|
||||||
|
case full(isEditable: Bool)
|
||||||
|
|
||||||
|
var isPreview: Bool {
|
||||||
|
switch self {
|
||||||
|
case .preview:
|
||||||
|
return true
|
||||||
|
case .full:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEditable: Bool {
|
||||||
|
switch self {
|
||||||
|
case .preview:
|
||||||
|
return false
|
||||||
|
case .full(let isEditable):
|
||||||
|
return isEditable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct PollView: View {
|
struct PollView: View {
|
||||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||||
|
|
||||||
let poll: Poll
|
let poll: Poll
|
||||||
let editable: Bool
|
let state: PollViewState
|
||||||
let actionHandler: (PollViewAction) -> Void
|
let actionHandler: (PollViewAction) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
if state.isPreview {
|
||||||
questionView
|
questionView
|
||||||
optionsView
|
} else {
|
||||||
summaryView
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
toolbarView
|
questionView
|
||||||
|
optionsView
|
||||||
|
summaryView
|
||||||
|
toolbarView
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 450)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 450)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@@ -88,7 +115,7 @@ struct PollView: View {
|
|||||||
Button {
|
Button {
|
||||||
toolbarAction()
|
toolbarAction()
|
||||||
} label: {
|
} label: {
|
||||||
Text(editable ? L10n.actionEditPoll : L10n.actionEndPoll)
|
Text(state.isEditable ? L10n.actionEditPoll : L10n.actionEndPoll)
|
||||||
.lineLimit(2, reservesSpace: false)
|
.lineLimit(2, reservesSpace: false)
|
||||||
.font(.compound.bodyLGSemibold)
|
.font(.compound.bodyLGSemibold)
|
||||||
.foregroundColor(.compound.textOnSolidPrimary)
|
.foregroundColor(.compound.textOnSolidPrimary)
|
||||||
@@ -105,7 +132,7 @@ struct PollView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func toolbarAction() {
|
private func toolbarAction() {
|
||||||
if editable {
|
if state.isEditable {
|
||||||
actionHandler(.edit)
|
actionHandler(.edit)
|
||||||
} else {
|
} else {
|
||||||
actionHandler(.end)
|
actionHandler(.end)
|
||||||
@@ -146,28 +173,32 @@ private extension Poll {
|
|||||||
|
|
||||||
struct PollView_Previews: PreviewProvider, TestablePreview {
|
struct PollView_Previews: PreviewProvider, TestablePreview {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PollView(poll: .disclosed(), editable: false) { _ in }
|
PollView(poll: .disclosed(), state: .full(isEditable: false)) { _ in }
|
||||||
.padding()
|
.padding()
|
||||||
.previewDisplayName("Disclosed")
|
.previewDisplayName("Disclosed")
|
||||||
|
|
||||||
PollView(poll: .undisclosed(), editable: false) { _ in }
|
PollView(poll: .undisclosed(), state: .full(isEditable: false)) { _ in }
|
||||||
.padding()
|
.padding()
|
||||||
.previewDisplayName("Undisclosed")
|
.previewDisplayName("Undisclosed")
|
||||||
|
|
||||||
PollView(poll: .endedDisclosed, editable: false) { _ in }
|
PollView(poll: .endedDisclosed, state: .full(isEditable: false)) { _ in }
|
||||||
.padding()
|
.padding()
|
||||||
.previewDisplayName("Ended, Disclosed")
|
.previewDisplayName("Ended, Disclosed")
|
||||||
|
|
||||||
PollView(poll: .endedUndisclosed, editable: false) { _ in }
|
PollView(poll: .endedUndisclosed, state: .full(isEditable: false)) { _ in }
|
||||||
.padding()
|
.padding()
|
||||||
.previewDisplayName("Ended, Undisclosed")
|
.previewDisplayName("Ended, Undisclosed")
|
||||||
|
|
||||||
PollView(poll: .disclosed(createdByAccountOwner: true), editable: true) { _ in }
|
PollView(poll: .disclosed(createdByAccountOwner: true), state: .full(isEditable: true)) { _ in }
|
||||||
.padding()
|
.padding()
|
||||||
.previewDisplayName("Creator, disclosed")
|
.previewDisplayName("Creator, disclosed")
|
||||||
|
|
||||||
PollView(poll: .emptyDisclosed, editable: true) { _ in }
|
PollView(poll: .emptyDisclosed, state: .full(isEditable: true)) { _ in }
|
||||||
.padding()
|
.padding()
|
||||||
.previewDisplayName("Creator, no votes")
|
.previewDisplayName("Creator, no votes")
|
||||||
|
|
||||||
|
PollView(poll: .emptyDisclosed, state: .preview) { _ in }
|
||||||
|
.padding()
|
||||||
|
.previewDisplayName("Preview")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
|
|||||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members))
|
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members))
|
||||||
let mock = TimelineViewModel(roomProxy: roomProxyMock,
|
let mock = TimelineViewModel(roomProxy: roomProxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
canCurrentUserPin: context.viewState.canCurrentUserPin,
|
canCurrentUserPin: context.viewState.canCurrentUserPin,
|
||||||
pinnedEventIDs: context.viewState.pinnedEventIDs,
|
pinnedEventIDs: context.viewState.pinnedEventIDs,
|
||||||
isDM: context.viewState.isEncryptedOneToOneRoom,
|
isDM: context.viewState.isEncryptedOneToOneRoom,
|
||||||
isViewSourceEnabled: context.viewState.isViewSourceEnabled)
|
isViewSourceEnabled: context.viewState.isViewSourceEnabled,
|
||||||
|
isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline)
|
||||||
TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in
|
TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in
|
||||||
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action))
|
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ struct TimelineItemStatusView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
if context.viewState.showReadReceipts, !timelineItem.properties.orderedReadReceipts.isEmpty {
|
if context.viewState.isPinnedEventsTimeline {
|
||||||
|
// Do not display any status when is a pinned events timeline
|
||||||
|
EmptyView()
|
||||||
|
} else if context.viewState.showReadReceipts, !timelineItem.properties.orderedReadReceipts.isEmpty {
|
||||||
readReceipts
|
readReceipts
|
||||||
} else {
|
} else {
|
||||||
deliveryStatusBadge
|
deliveryStatusBadge
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
static let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)),
|
static let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)),
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
+1
@@ -99,6 +99,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
|||||||
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
||||||
focussedEventID: focussedEventID,
|
focussedEventID: focussedEventID,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
+15
-1
@@ -20,9 +20,18 @@ struct PollRoomTimelineView: View {
|
|||||||
let timelineItem: PollRoomTimelineItem
|
let timelineItem: PollRoomTimelineItem
|
||||||
@EnvironmentObject private var context: TimelineViewModel.Context
|
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||||
|
|
||||||
|
private var state: PollViewState {
|
||||||
|
if context.viewState.isPinnedEventsTimeline {
|
||||||
|
return .preview
|
||||||
|
} else {
|
||||||
|
return .full(isEditable: timelineItem.isEditable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
PollView(poll: poll, editable: timelineItem.isEditable) { action in
|
PollView(poll: poll,
|
||||||
|
state: state) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .selectOption(let optionID):
|
case .selectOption(let optionID):
|
||||||
guard let eventID, let option = poll.options.first(where: { $0.id == optionID }), !option.isSelected else { return }
|
guard let eventID, let option = poll.options.first(where: { $0.id == optionID }), !option.isSelected else { return }
|
||||||
@@ -51,6 +60,7 @@ struct PollRoomTimelineView: View {
|
|||||||
|
|
||||||
struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let pinnedEventsTimelineViewModel = TimelineViewModel.pinnedEventsTimelineMock
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
|
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
|
||||||
@@ -76,5 +86,9 @@ struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
PollRoomTimelineView(timelineItem: .mock(poll: .emptyDisclosed, isEditable: true))
|
PollRoomTimelineView(timelineItem: .mock(poll: .emptyDisclosed, isEditable: true))
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
.previewDisplayName("Creator, no votes, Bubble")
|
.previewDisplayName("Creator, no votes, Bubble")
|
||||||
|
|
||||||
|
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isEditable: true))
|
||||||
|
.environmentObject(pinnedEventsTimelineViewModel.context)
|
||||||
|
.previewDisplayName("Preview")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
|
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
|
||||||
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: false)
|
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: true)
|
||||||
await timeline.subscribeForUpdates()
|
await timeline.subscribeForUpdates()
|
||||||
innerPinnedEventsTimeline = timeline
|
innerPinnedEventsTimeline = timeline
|
||||||
return timeline
|
return timeline
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ struct GeoURI: Hashable {
|
|||||||
let latitude: Double
|
let latitude: Double
|
||||||
let longitude: Double
|
let longitude: Double
|
||||||
let uncertainty: Double?
|
let uncertainty: Double?
|
||||||
|
|
||||||
|
var bodyMessage: String {
|
||||||
|
"Location was shared at \(string)"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
private let serialDispatchQueue: DispatchQueue
|
private let serialDispatchQueue: DispatchQueue
|
||||||
|
private let shouldHideStart: Bool
|
||||||
|
|
||||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||||
|
|
||||||
@@ -44,12 +45,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
init(roomProxy: JoinedRoomProxyProtocol,
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
timelineProxy: TimelineProxyProtocol,
|
timelineProxy: TimelineProxyProtocol,
|
||||||
initialFocussedEventID: String?,
|
initialFocussedEventID: String?,
|
||||||
|
shouldHideStart: Bool,
|
||||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||||
appSettings: AppSettings) {
|
appSettings: AppSettings) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
liveTimelineProvider = timelineProxy.timelineProvider
|
liveTimelineProvider = timelineProxy.timelineProvider
|
||||||
self.timelineItemFactory = timelineItemFactory
|
self.timelineItemFactory = timelineItemFactory
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
|
self.shouldHideStart = shouldHideStart
|
||||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
|
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
|
||||||
|
|
||||||
activeTimeline = timelineProxy
|
activeTimeline = timelineProxy
|
||||||
@@ -364,7 +367,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
// Check if we need to add anything to the top of the timeline.
|
// Check if we need to add anything to the top of the timeline.
|
||||||
switch paginationState.backward {
|
switch paginationState.backward {
|
||||||
case .timelineEndReached:
|
case .timelineEndReached:
|
||||||
if !roomProxy.isEncryptedOneToOneRoom {
|
if !shouldHideStart, !roomProxy.isEncryptedOneToOneRoom {
|
||||||
let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name)
|
let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name)
|
||||||
newTimelineItems.insert(timelineStart, at: 0)
|
newTimelineItems.insert(timelineStart, at: 0)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -23,6 +23,7 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
|
|||||||
RoomTimelineController(roomProxy: roomProxy,
|
RoomTimelineController(roomProxy: roomProxy,
|
||||||
timelineProxy: roomProxy.timeline,
|
timelineProxy: roomProxy.timeline,
|
||||||
initialFocussedEventID: initialFocussedEventID,
|
initialFocussedEventID: initialFocussedEventID,
|
||||||
|
shouldHideStart: false,
|
||||||
timelineItemFactory: timelineItemFactory,
|
timelineItemFactory: timelineItemFactory,
|
||||||
appSettings: ServiceLocator.shared.settings)
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
|
|||||||
return RoomTimelineController(roomProxy: roomProxy,
|
return RoomTimelineController(roomProxy: roomProxy,
|
||||||
timelineProxy: pinnedEventsTimeline,
|
timelineProxy: pinnedEventsTimeline,
|
||||||
initialFocussedEventID: nil,
|
initialFocussedEventID: nil,
|
||||||
|
shouldHideStart: true,
|
||||||
timelineItemFactory: timelineItemFactory,
|
timelineItemFactory: timelineItemFactory,
|
||||||
appSettings: ServiceLocator.shared.settings)
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ class MockScreen: Identifiable {
|
|||||||
let client = try UITestsSignalling.Client(mode: .app)
|
let client = try UITestsSignalling.Client(mode: .app)
|
||||||
client.signals.sink { [weak self] signal in
|
client.signals.sink { [weak self] signal in
|
||||||
guard case .timeline(.focusOnEvent(let eventID)) = signal else { return }
|
guard case .timeline(.focusOnEvent(let eventID)) = signal else { return }
|
||||||
coordinator.focusOnEvent(eventID: eventID)
|
coordinator.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
||||||
try? client.send(.success)
|
try? client.send(.success)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -649,6 +649,7 @@ class MockScreen: Identifiable {
|
|||||||
let timelineController = RoomTimelineController(roomProxy: roomProxy,
|
let timelineController = RoomTimelineController(roomProxy: roomProxy,
|
||||||
timelineProxy: roomProxy.timeline,
|
timelineProxy: roomProxy.timeline,
|
||||||
initialFocussedEventID: nil,
|
initialFocussedEventID: nil,
|
||||||
|
shouldHideStart: false,
|
||||||
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
|
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
|
||||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")),
|
stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")),
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:901024e6cd60483e2af6001b87f72cb98fc347cd53c82097f232f602994ea5f7
|
||||||
|
size 79270
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:901024e6cd60483e2af6001b87f72cb98fc347cd53c82097f232f602994ea5f7
|
||||||
|
size 79270
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7dd720bde5e4e8496a48d89e624017904e1c7861187698488ddd53e211963171
|
||||||
|
size 39185
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7dd720bde5e4e8496a48d89e624017904e1c7861187698488ddd53e211963171
|
||||||
|
size 39185
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:88c3bb5b185ac4c183744884d63de94a2554872e317e09b9454fc895d2aa2035
|
||||||
|
size 74948
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:88c3bb5b185ac4c183744884d63de94a2554872e317e09b9454fc895d2aa2035
|
||||||
|
size 74948
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:76c12d5338c4d8832169ac2c06d3ef37b91e66a02b5e57b61a14d6bd0b546def
|
||||||
|
size 33674
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:76c12d5338c4d8832169ac2c06d3ef37b91e66a02b5e57b61a14d6bd0b546def
|
||||||
|
size 33674
|
||||||
@@ -28,6 +28,7 @@ class PillContextTests: XCTestCase {
|
|||||||
proxyMock.membersPublisher = subject.asCurrentValuePublisher()
|
proxyMock.membersPublisher = subject.asCurrentValuePublisher()
|
||||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -56,6 +57,7 @@ class PillContextTests: XCTestCase {
|
|||||||
proxyMock.membersPublisher = subject.asCurrentValuePublisher()
|
proxyMock.membersPublisher = subject.asCurrentValuePublisher()
|
||||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -77,6 +79,7 @@ class PillContextTests: XCTestCase {
|
|||||||
mockController.roomProxy = proxyMock
|
mockController.roomProxy = proxyMock
|
||||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||||
timelineController: mockController,
|
timelineController: mockController,
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ class PinnedEventsBannerStateTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertTrue(state.isLoading)
|
XCTAssertTrue(state.isLoading)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
XCTAssertNil(state.selectedPinEventID)
|
XCTAssertNil(state.selectedPinnedEventID)
|
||||||
XCTAssertEqual(state.displayedMessage.string, L10n.screenRoomPinnedBannerLoadingDescription)
|
XCTAssertEqual(state.displayedMessage.string, L10n.screenRoomPinnedBannerLoadingDescription)
|
||||||
XCTAssertEqual(state.selectedPinIndex, 4)
|
XCTAssertEqual(state.selectedPinnedIndex, 4)
|
||||||
XCTAssertEqual(state.count, 5)
|
XCTAssertEqual(state.count, 5)
|
||||||
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(5, 5)))
|
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(5, 5)))
|
||||||
}
|
}
|
||||||
@@ -48,42 +48,42 @@ class PinnedEventsBannerStateTests: XCTestCase {
|
|||||||
var state = PinnedEventsBannerState.loading(numbersOfEvents: 2)
|
var state = PinnedEventsBannerState.loading(numbersOfEvents: 2)
|
||||||
XCTAssertTrue(state.isLoading)
|
XCTAssertTrue(state.isLoading)
|
||||||
state.setPinnedEventContents(["1": "test1", "2": "test2"])
|
state.setPinnedEventContents(["1": "test1", "2": "test2"])
|
||||||
XCTAssertEqual(state, .loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinEventID: "2")))
|
XCTAssertEqual(state, .loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2")))
|
||||||
XCTAssertFalse(state.isLoading)
|
XCTAssertFalse(state.isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoaded() {
|
func testLoaded() {
|
||||||
let state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinEventID: "2"))
|
let state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2"))
|
||||||
XCTAssertFalse(state.isLoading)
|
XCTAssertFalse(state.isLoading)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
XCTAssertEqual(state.selectedPinEventID, "2")
|
XCTAssertEqual(state.selectedPinnedEventID, "2")
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test2")
|
XCTAssertEqual(state.displayedMessage.string, "test2")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 1)
|
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||||
XCTAssertEqual(state.count, 2)
|
XCTAssertEqual(state.count, 2)
|
||||||
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(2, 2)))
|
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(2, 2)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPreviousPin() {
|
func testPreviousPin() {
|
||||||
var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3"], selectedPinEventID: "1"))
|
var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3"], selectedPinnedEventID: "1"))
|
||||||
XCTAssertEqual(state.selectedPinEventID, "1")
|
XCTAssertEqual(state.selectedPinnedEventID, "1")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 0)
|
XCTAssertEqual(state.selectedPinnedIndex, 0)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test1")
|
XCTAssertEqual(state.displayedMessage.string, "test1")
|
||||||
|
|
||||||
state.previousPin()
|
state.previousPin()
|
||||||
XCTAssertEqual(state.selectedPinEventID, "3")
|
XCTAssertEqual(state.selectedPinnedEventID, "3")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 2)
|
XCTAssertEqual(state.selectedPinnedIndex, 2)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test3")
|
XCTAssertEqual(state.displayedMessage.string, "test3")
|
||||||
|
|
||||||
state.previousPin()
|
state.previousPin()
|
||||||
XCTAssertEqual(state.selectedPinEventID, "2")
|
XCTAssertEqual(state.selectedPinnedEventID, "2")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 1)
|
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test2")
|
XCTAssertEqual(state.displayedMessage.string, "test2")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSetContent() {
|
func testSetContent() {
|
||||||
var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3", "4": "test4"], selectedPinEventID: "2"))
|
var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3", "4": "test4"], selectedPinnedEventID: "2"))
|
||||||
XCTAssertEqual(state.selectedPinEventID, "2")
|
XCTAssertEqual(state.selectedPinnedEventID, "2")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 1)
|
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test2")
|
XCTAssertEqual(state.displayedMessage.string, "test2")
|
||||||
XCTAssertEqual(state.count, 4)
|
XCTAssertEqual(state.count, 4)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
@@ -91,8 +91,8 @@ class PinnedEventsBannerStateTests: XCTestCase {
|
|||||||
// let's remove the selected item
|
// let's remove the selected item
|
||||||
state.setPinnedEventContents(["1": "test1", "3": "test3", "4": "test4"])
|
state.setPinnedEventContents(["1": "test1", "3": "test3", "4": "test4"])
|
||||||
// new selected item is the new latest
|
// new selected item is the new latest
|
||||||
XCTAssertEqual(state.selectedPinEventID, "4")
|
XCTAssertEqual(state.selectedPinnedEventID, "4")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 2)
|
XCTAssertEqual(state.selectedPinnedIndex, 2)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test4")
|
XCTAssertEqual(state.displayedMessage.string, "test4")
|
||||||
XCTAssertEqual(state.count, 3)
|
XCTAssertEqual(state.count, 3)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
@@ -100,9 +100,9 @@ class PinnedEventsBannerStateTests: XCTestCase {
|
|||||||
// let's add a new item at the top
|
// let's add a new item at the top
|
||||||
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4"])
|
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4"])
|
||||||
// selected item doesn't change
|
// selected item doesn't change
|
||||||
XCTAssertEqual(state.selectedPinEventID, "4")
|
XCTAssertEqual(state.selectedPinnedEventID, "4")
|
||||||
// but the index is updated
|
// but the index is updated
|
||||||
XCTAssertEqual(state.selectedPinIndex, 3)
|
XCTAssertEqual(state.selectedPinnedIndex, 3)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test4")
|
XCTAssertEqual(state.displayedMessage.string, "test4")
|
||||||
XCTAssertEqual(state.count, 4)
|
XCTAssertEqual(state.count, 4)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
@@ -110,9 +110,9 @@ class PinnedEventsBannerStateTests: XCTestCase {
|
|||||||
// let's add a new item at the bottom
|
// let's add a new item at the bottom
|
||||||
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4", "5": "test5"])
|
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4", "5": "test5"])
|
||||||
// selected item doesn't change
|
// selected item doesn't change
|
||||||
XCTAssertEqual(state.selectedPinEventID, "4")
|
XCTAssertEqual(state.selectedPinnedEventID, "4")
|
||||||
// and index stays the same
|
// and index stays the same
|
||||||
XCTAssertEqual(state.selectedPinIndex, 3)
|
XCTAssertEqual(state.selectedPinnedIndex, 3)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test4")
|
XCTAssertEqual(state.displayedMessage.string, "test4")
|
||||||
XCTAssertEqual(state.count, 5)
|
XCTAssertEqual(state.count, 5)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
@@ -120,12 +120,12 @@ class PinnedEventsBannerStateTests: XCTestCase {
|
|||||||
// set to tempty
|
// set to tempty
|
||||||
state.setPinnedEventContents([:])
|
state.setPinnedEventContents([:])
|
||||||
XCTAssertTrue(state.isEmpty)
|
XCTAssertTrue(state.isEmpty)
|
||||||
XCTAssertNil(state.selectedPinEventID)
|
XCTAssertNil(state.selectedPinnedEventID)
|
||||||
|
|
||||||
// set to one item
|
// set to one item
|
||||||
state.setPinnedEventContents(["6": "test6", "7": "test7"])
|
state.setPinnedEventContents(["6": "test6", "7": "test7"])
|
||||||
XCTAssertEqual(state.selectedPinEventID, "7")
|
XCTAssertEqual(state.selectedPinnedEventID, "7")
|
||||||
XCTAssertEqual(state.selectedPinIndex, 1)
|
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||||
XCTAssertEqual(state.displayedMessage.string, "test7")
|
XCTAssertEqual(state.displayedMessage.string, "test7")
|
||||||
XCTAssertEqual(state.count, 2)
|
XCTAssertEqual(state.count, 2)
|
||||||
XCTAssertFalse(state.isEmpty)
|
XCTAssertFalse(state.isEmpty)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
// setup the room proxy actions publisher
|
// setup the room proxy actions publisher
|
||||||
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher()
|
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher()
|
||||||
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
appMediator: AppMediatorMock.default,
|
appMediator: AppMediatorMock.default,
|
||||||
@@ -67,6 +68,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 1)
|
||||||
|
|
||||||
// setup the loaded pinned events injection in the timeline
|
// setup the loaded pinned events injection in the timeline
|
||||||
let pinnedTimelineMock = TimelineProxyMock()
|
let pinnedTimelineMock = TimelineProxyMock()
|
||||||
@@ -74,8 +76,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>()
|
let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>()
|
||||||
pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher()
|
pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher()
|
||||||
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
|
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
|
||||||
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItemSDKMock(configuration: .init()), id: "1")),
|
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test1")), id: "1")),
|
||||||
.event(.init(item: EventTimelineItemSDKMock(configuration: .init()), id: "2"))]
|
.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test2")), id: "2"))]
|
||||||
|
|
||||||
// check if the banner is now in a loaded state and is showing the counter
|
// check if the banner is now in a loaded state and is showing the counter
|
||||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
@@ -85,18 +87,20 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 2)
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 2)
|
||||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 1)
|
||||||
|
|
||||||
// check if the banner is updating alongside the timeline
|
// check if the banner is updating alongside the timeline
|
||||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
viewState.pinnedEventsBannerState.count == 3
|
viewState.pinnedEventsBannerState.count == 3
|
||||||
}
|
}
|
||||||
providerUpdateSubject.send(([.event(.init(item: EventTimelineItemSDKMock(configuration: .init()), id: "1")),
|
providerUpdateSubject.send(([.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test1")), id: "1")),
|
||||||
.event(.init(item: EventTimelineItemSDKMock(configuration: .init()), id: "2")),
|
.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test2")), id: "2")),
|
||||||
.event(.init(item: EventTimelineItemSDKMock(configuration: .init()), id: "3"))], .initial))
|
.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test3")), id: "3"))], .initial))
|
||||||
|
try await deferred.fulfill()
|
||||||
XCTAssertFalse(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
XCTAssertFalse(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
try await deferred.fulfill()
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 1)
|
||||||
|
|
||||||
// check how the scrolling changes the banner visibility
|
// check how the scrolling changes the banner visibility
|
||||||
viewModel.timelineHasScrolled(direction: .top)
|
viewModel.timelineHasScrolled(direction: .top)
|
||||||
XCTAssertFalse(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
XCTAssertFalse(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
@@ -105,6 +109,61 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPinnedEventsBannerSelection() async throws {
|
||||||
|
ServiceLocator.shared.settings.pinningEnabled = true
|
||||||
|
let timelineSubject = PassthroughSubject<TimelineProxyProtocol, Never>()
|
||||||
|
let updateSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init())
|
||||||
|
// setup a way to inject the mock of the pinned events timeline
|
||||||
|
let pinnedTimelineMock = TimelineProxyMock()
|
||||||
|
let pinnedTimelineProviderMock = RoomTimelineProviderMock()
|
||||||
|
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
|
||||||
|
pinnedTimelineProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher()
|
||||||
|
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test1")), id: "1")),
|
||||||
|
.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test2")), id: "2")),
|
||||||
|
.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test3")), id: "3"))]
|
||||||
|
roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock
|
||||||
|
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: "test1",
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analyticsService: ServiceLocator.shared.analytics)
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
// check if the banner is now in a loaded state and is showing the counter
|
||||||
|
var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
|
!viewState.pinnedEventsBannerState.isLoading
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 3)
|
||||||
|
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
|
// And that is actually displaying the `initialSelectedPinEventID` which is gthe first one in the list
|
||||||
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 0)
|
||||||
|
|
||||||
|
// check if the banner scrolls when tapping the previous pin
|
||||||
|
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
|
viewState.pinnedEventsBannerState.selectedPinnedIndex == 2
|
||||||
|
}
|
||||||
|
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||||
|
if case let .focusEvent(eventID) = action {
|
||||||
|
return eventID == "test1"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
viewModel.context.send(viewAction: .tappedPinnedEventsBanner)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
try await deferredAction.fulfill()
|
||||||
|
|
||||||
|
// check if the banner scrolls to the specific selected pin
|
||||||
|
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
|
viewState.pinnedEventsBannerState.selectedPinnedIndex == 1
|
||||||
|
}
|
||||||
|
viewModel.setSelectedPinnedEventID("test2")
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
func testRoomInfoUpdate() async throws {
|
func testRoomInfoUpdate() async throws {
|
||||||
let updateSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
|
let updateSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
|
||||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false))
|
let roomProxyMock = JoinedRoomProxyMock(.init(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false))
|
||||||
@@ -112,6 +171,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false)
|
roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false)
|
||||||
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher()
|
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher()
|
||||||
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
appMediator: AppMediatorMock.default,
|
appMediator: AppMediatorMock.default,
|
||||||
@@ -148,6 +208,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
let ongoingCallRoomIDSubject = CurrentValueSubject<String?, Never>(nil)
|
let ongoingCallRoomIDSubject = CurrentValueSubject<String?, Never>(nil)
|
||||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID"))
|
let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID"))
|
||||||
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(),
|
ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(),
|
||||||
appMediator: AppMediatorMock.default,
|
appMediator: AppMediatorMock.default,
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
let viewModel = TimelineViewModel(roomProxy: roomProxy,
|
let viewModel = TimelineViewModel(roomProxy: roomProxy,
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -369,6 +370,7 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
timelineController.timelineItems = [message]
|
timelineController.timelineItems = [message]
|
||||||
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])),
|
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -394,6 +396,7 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
|
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
|
||||||
let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -422,6 +425,7 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
|
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
|
||||||
let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
@@ -451,6 +455,7 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")),
|
TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")),
|
||||||
focussedEventID: focussedEventID,
|
focussedEventID: focussedEventID,
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
|
isPinnedEventsTimeline: false,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||||
|
|||||||
Reference in New Issue
Block a user