Add support for starting voice calls from a DM (#5305)
* feat: Start voice call from DM * rename voiceCall:bool to isVoiceCall * review: Fix a typo * review: use one displayCall(bool) instead of 2 actions * review: Add a new specific preview for DM calls * combine startCall and startVoiceCall in single enum with isVoiceCall * review: add isVoiceCall to presentCallScreen action * review: Use proper a11y for voice vs video * add voice/video options to UserProfile Screen * fixup: move config params to the roomInfo object * review: Revert changes on preview as the toolbar cannot be snapshot'd * review: Extract call controls in specific file * oups: Add voice call option in room details screen * Update room details screenshots * Update user profile screenshots * Update room member details screenshots * fixup: remove dead code
This commit is contained in:
@@ -16,7 +16,7 @@ enum ChatsTabFlowCoordinatorAction {
|
||||
case showSettings
|
||||
case showChatBackupSettings
|
||||
case sessionVerification(SessionVerificationScreenFlow)
|
||||
case showCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case showCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case hideCallScreenOverlay
|
||||
case logout
|
||||
}
|
||||
@@ -535,8 +535,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
|
||||
case .continueWithSpaceFlow(let spaceRoomListProxy):
|
||||
@@ -597,8 +597,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
|
||||
case .finished:
|
||||
@@ -800,8 +800,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .openDirectChat(let roomID):
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
|
||||
case .startCall(let roomProxy):
|
||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy))
|
||||
case .startCall(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .dismiss:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
enum RoomFlowCoordinatorAction: Equatable {
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case verifyUser(userID: String)
|
||||
/// The requested room was actually a space. The room flow has been dismissed
|
||||
/// and a space flow should be started to continue.
|
||||
@@ -21,8 +21,8 @@ enum RoomFlowCoordinatorAction: Equatable {
|
||||
|
||||
static func == (lhs: RoomFlowCoordinatorAction, rhs: RoomFlowCoordinatorAction) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.presentCallScreen(let lhsRoomProxy), .presentCallScreen(let rhsRoomProxy)):
|
||||
lhsRoomProxy.id == rhsRoomProxy.id
|
||||
case (.presentCallScreen(let lhsRoomProxy, let lhsIsVoiceCall), .presentCallScreen(let rhsRoomProxy, let rhsIsVoiceCall)):
|
||||
lhsRoomProxy.id == rhsRoomProxy.id && lhsIsVoiceCall == rhsIsVoiceCall
|
||||
case (.finished, .finished):
|
||||
true
|
||||
default:
|
||||
@@ -717,7 +717,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .presentMessageForwarding(let forwardingItem):
|
||||
stateMachine.tryEvent(.presentMessageForwarding(forwardingItem: forwardingItem))
|
||||
case .presentCallScreen:
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
|
||||
case .presentPinnedEventsTimeline:
|
||||
stateMachine.tryEvent(.presentPinnedEventsTimeline)
|
||||
case .presentResolveSendFailure(failure: let failure, sendHandle: let sendHandle):
|
||||
@@ -952,8 +952,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentPollsHistory)
|
||||
case .presentRolesAndPermissionsScreen:
|
||||
stateMachine.tryEvent(.presentRolesAndPermissionsScreen)
|
||||
case .presentCall:
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCall(isVoiceCall: let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .presentPinnedEventsTimeline:
|
||||
stateMachine.tryEvent(.presentPinnedEventsTimeline)
|
||||
case .presentKnockingRequestsListScreen:
|
||||
@@ -1505,8 +1505,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .continueWithSpaceFlow(let spaceRoomListProxy):
|
||||
@@ -1615,8 +1615,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .finished:
|
||||
@@ -1641,8 +1641,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.stopMembersFlow)
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
enum RoomMembersFlowCoordinatorAction {
|
||||
case finished
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case verifyUser(userID: String)
|
||||
}
|
||||
|
||||
@@ -235,8 +235,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentUserProfile(userID: userID))
|
||||
case .openDirectChat(let roomID):
|
||||
stateMachine.tryEvent(.startRoomFlow(roomID: roomID, via: [], eventID: nil))
|
||||
case .startCall(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .startCall(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
}
|
||||
@@ -294,8 +294,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .openDirectChat(let roomID):
|
||||
stateMachine.tryEvent(.startRoomFlow(roomID: roomID, via: [], eventID: nil))
|
||||
case .startCall(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .startCall(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .dismiss:
|
||||
break // Not supported when pushed.
|
||||
}
|
||||
@@ -322,8 +322,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .continueWithSpaceFlow:
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
import SwiftState
|
||||
|
||||
enum SpaceFlowCoordinatorAction {
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case verifyUser(userID: String)
|
||||
case finished
|
||||
}
|
||||
@@ -504,8 +504,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .finished:
|
||||
@@ -529,8 +529,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .continueWithSpaceFlow(let spaceRoomListProxy):
|
||||
@@ -557,8 +557,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch actions {
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.stopMembersFlow)
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
}
|
||||
@@ -582,7 +582,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.leftSpace)
|
||||
}
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
|
||||
case .verifyUser(userID: let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.stopMembersListFlow)
|
||||
case .presentCallScreen(let roomProxy):
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftState
|
||||
|
||||
enum SpacesTabFlowCoordinatorAction {
|
||||
case showSettings
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case verifyUser(userID: String)
|
||||
}
|
||||
|
||||
@@ -178,8 +178,8 @@ class SpacesTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .finished:
|
||||
|
||||
@@ -207,8 +207,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
handleAppRoute(.chatBackupSettings, animated: true)
|
||||
case .sessionVerification(let flow):
|
||||
presentSessionVerificationScreen(flow: flow)
|
||||
case .showCallScreen(let roomProxy):
|
||||
presentCallScreen(roomProxy: roomProxy, voiceOnly: false)
|
||||
case .showCallScreen(let roomProxy, let isVoiceCall):
|
||||
presentCallScreen(roomProxy: roomProxy, voiceOnly: isVoiceCall)
|
||||
case .hideCallScreenOverlay:
|
||||
hideCallScreenOverlay()
|
||||
case .logout:
|
||||
@@ -221,8 +221,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
presentCallScreen(roomProxy: roomProxy, voiceOnly: false)
|
||||
case .presentCallScreen(let roomProxy, let isVoiceCall):
|
||||
presentCallScreen(roomProxy: roomProxy, voiceOnly: isVoiceCall)
|
||||
case .verifyUser(let userID):
|
||||
presentSessionVerificationScreen(flow: .userInitiator(userID: userID))
|
||||
case .showSettings:
|
||||
|
||||
@@ -120,6 +120,8 @@ internal enum L10n {
|
||||
internal static var a11yShowPassword: String { return L10n.tr("Localizable", "a11y_show_password") }
|
||||
/// Start a call
|
||||
internal static var a11yStartCall: String { return L10n.tr("Localizable", "a11y_start_call") }
|
||||
/// Start a video call
|
||||
internal static var a11yStartVideoCall: String { return L10n.tr("Localizable", "a11y_start_video_call") }
|
||||
/// Start a voice call
|
||||
internal static var a11yStartVoiceCall: String { return L10n.tr("Localizable", "a11y_start_voice_call") }
|
||||
/// Tombstoned room
|
||||
|
||||
@@ -157,6 +157,8 @@ enum A11yIdentifiers {
|
||||
let attachmentPickerTextFormatting = "room-attachment_picker_text_formatting"
|
||||
let timelineItemActionMenu = "room-timeline_item_action_menu"
|
||||
let joinCall = "room-join_call"
|
||||
let startVoiceCall = "room-start_voice_call"
|
||||
let startVideoCall = "room-start_video_call"
|
||||
let scrollToBottom = "room-scroll_to_bottom"
|
||||
|
||||
let messageComposer = "room-message_composer"
|
||||
|
||||
@@ -28,7 +28,7 @@ enum RoomDetailsScreenCoordinatorAction {
|
||||
case presentInviteUsersScreen
|
||||
case presentPollsHistory
|
||||
case presentRolesAndPermissionsScreen
|
||||
case presentCall
|
||||
case presentCall(isVoiceCall: Bool)
|
||||
case presentPinnedEventsTimeline
|
||||
case presentMediaEventsTimeline
|
||||
case presentKnockingRequestsListScreen
|
||||
@@ -81,8 +81,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentPollsHistory)
|
||||
case .requestRolesAndPermissionsPresentation:
|
||||
actionsSubject.send(.presentRolesAndPermissionsScreen)
|
||||
case .startCall:
|
||||
actionsSubject.send(.presentCall)
|
||||
case .startCall(let isVoiceCall):
|
||||
actionsSubject.send(.presentCall(isVoiceCall: isVoiceCall))
|
||||
case .displayPinnedEventsTimeline:
|
||||
actionsSubject.send(.presentPinnedEventsTimeline)
|
||||
case .displayMediaEventsTimeline:
|
||||
|
||||
@@ -22,7 +22,7 @@ enum RoomDetailsScreenViewModelAction: Equatable {
|
||||
case requestEditDetailsPresentation
|
||||
case requestPollsHistoryPresentation
|
||||
case requestRolesAndPermissionsPresentation
|
||||
case startCall
|
||||
case startCall(isVoiceCall: Bool)
|
||||
case displayPinnedEventsTimeline
|
||||
case displayMediaEventsTimeline
|
||||
case displayKnockingRequests
|
||||
@@ -94,7 +94,10 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
var shortcuts: [RoomDetailsScreenViewShortcut] {
|
||||
var shortcuts: [RoomDetailsScreenViewShortcut] = [.mute]
|
||||
if !ProcessInfo.processInfo.isiOSAppOnMac, canJoinCall {
|
||||
shortcuts.append(.call)
|
||||
if isDirect {
|
||||
shortcuts.append(.voiceCall)
|
||||
}
|
||||
shortcuts.append(.videoCall)
|
||||
}
|
||||
if dmRecipientInfo == nil, canInviteUsers {
|
||||
shortcuts.append(.invite)
|
||||
@@ -223,7 +226,7 @@ enum RoomDetailsScreenViewAction {
|
||||
case toggleFavourite(isFavourite: Bool)
|
||||
case processTapRolesAndPermissions
|
||||
case processTapSecurityAndPrivacy
|
||||
case processTapCall
|
||||
case processTapCall(isVoiceCall: Bool)
|
||||
case processTapPinnedEvents
|
||||
case processTapMediaEvents
|
||||
case processTapRequestsToJoin
|
||||
@@ -233,7 +236,8 @@ enum RoomDetailsScreenViewAction {
|
||||
enum RoomDetailsScreenViewShortcut {
|
||||
case share(link: URL)
|
||||
case mute
|
||||
case call
|
||||
case videoCall
|
||||
case voiceCall
|
||||
case invite
|
||||
}
|
||||
|
||||
|
||||
@@ -151,8 +151,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
Task { await toggleFavourite(isFavourite) }
|
||||
case .processTapRolesAndPermissions:
|
||||
actionsSubject.send(.requestRolesAndPermissionsPresentation)
|
||||
case .processTapCall:
|
||||
actionsSubject.send(.startCall)
|
||||
case .processTapCall(let isVoiceCall):
|
||||
actionsSubject.send(.startCall(isVoiceCall: isVoiceCall))
|
||||
case .processTapPinnedEvents:
|
||||
analyticsService.trackInteraction(name: .PinnedMessageRoomInfoButton)
|
||||
actionsSubject.send(.displayPinnedEventsTimeline)
|
||||
|
||||
@@ -85,13 +85,22 @@ struct RoomDetailsScreen: View {
|
||||
CompoundIcon(\.shareIos)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
|
||||
case .call:
|
||||
case .voiceCall:
|
||||
Button {
|
||||
context.send(viewAction: .processTapCall)
|
||||
context.send(viewAction: .processTapCall(isVoiceCall: true))
|
||||
} label: {
|
||||
CompoundIcon(\.voiceCall)
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yStartVoiceCall)
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
case .videoCall:
|
||||
Button {
|
||||
context.send(viewAction: .processTapCall(isVoiceCall: false))
|
||||
} label: {
|
||||
CompoundIcon(\.videoCall)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
.accessibilityLabel(L10n.a11yStartVideoCall)
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.commonVideo))
|
||||
case .invite:
|
||||
Button {
|
||||
context.send(viewAction: .processTapInvite)
|
||||
|
||||
@@ -20,7 +20,7 @@ struct RoomMemberDetailsScreenCoordinatorParameters {
|
||||
enum RoomMemberDetailsScreenCoordinatorAction {
|
||||
case openUserProfile
|
||||
case openDirectChat(roomID: String)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case verifyUser(userID: String)
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.openUserProfile)
|
||||
case .openDirectChat(let roomID):
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
case .startCall(let roomProxy):
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy))
|
||||
case .startCall(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
enum RoomMemberDetailsScreenViewModelAction {
|
||||
case openUserProfile
|
||||
case openDirectChat(roomID: String)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case verifyUser(userID: String)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ enum RoomMemberDetailsScreenViewAction {
|
||||
case displayAvatar(URL)
|
||||
case openDirectChat
|
||||
case createDirectChat
|
||||
case startCall(roomID: String)
|
||||
case startCall(roomID: String, isVoiceCall: Bool)
|
||||
case verifyUser
|
||||
case withdrawVerification
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
openDirectChat()
|
||||
case .createDirectChat:
|
||||
Task { await createDirectChat() }
|
||||
case .startCall(let roomID):
|
||||
Task { await startCall(roomID: roomID) }
|
||||
case .startCall(let roomID, let isVoiceCall):
|
||||
Task { await startCall(roomID: roomID, isVoiceCall: isVoiceCall) }
|
||||
case .verifyUser:
|
||||
actionsSubject.send(.verifyUser(userID: state.userID))
|
||||
case .withdrawVerification:
|
||||
@@ -225,12 +225,12 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
}
|
||||
}
|
||||
|
||||
private func startCall(roomID: String) async {
|
||||
private func startCall(roomID: String, isVoiceCall: Bool) async {
|
||||
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
showErrorIndicator()
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy))
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
}
|
||||
|
||||
// MARK: User Indicators
|
||||
|
||||
@@ -103,11 +103,20 @@ struct RoomMemberDetailsScreen: View {
|
||||
|
||||
if let roomID = context.viewState.dmRoomID {
|
||||
Button {
|
||||
context.send(viewAction: .startCall(roomID: roomID))
|
||||
context.send(viewAction: .startCall(roomID: roomID, isVoiceCall: true))
|
||||
} label: {
|
||||
CompoundIcon(\.voiceCall)
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yStartVoiceCall)
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
|
||||
Button {
|
||||
context.send(viewAction: .startCall(roomID: roomID, isVoiceCall: false))
|
||||
} label: {
|
||||
CompoundIcon(\.videoCall)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
.accessibilityLabel(L10n.a11yStartVideoCall)
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.commonVideo))
|
||||
}
|
||||
|
||||
if let permalink = context.viewState.memberDetails?.permalink {
|
||||
|
||||
@@ -43,7 +43,7 @@ enum RoomScreenCoordinatorAction {
|
||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
||||
case presentRoomMemberDetails(userID: String)
|
||||
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case presentCallScreen
|
||||
case presentCallScreen(isVoiceCall: Bool)
|
||||
case presentPinnedEventsTimeline
|
||||
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||
case presentKnockRequestsList
|
||||
@@ -178,8 +178,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentPinnedEventsTimeline)
|
||||
case .displayRoomDetails:
|
||||
actionsSubject.send(.presentRoomDetails)
|
||||
case .displayCall:
|
||||
actionsSubject.send(.presentCallScreen)
|
||||
case .displayCall(let isVoiceCall):
|
||||
actionsSubject.send(.presentCallScreen(isVoiceCall: isVoiceCall))
|
||||
case .removeComposerFocus:
|
||||
composerViewModel.process(timelineAction: .removeFocus)
|
||||
case .displayKnockRequests:
|
||||
|
||||
@@ -16,7 +16,7 @@ enum RoomScreenViewModelAction: Equatable {
|
||||
case displayThread(threadRootEventID: String, focussedEventID: String)
|
||||
case displayPinnedEventsTimeline
|
||||
case displayRoomDetails
|
||||
case displayCall
|
||||
case displayCall(isVoiceCall: Bool)
|
||||
case removeComposerFocus
|
||||
case displayKnockRequests
|
||||
case displayRoom(roomID: String, via: [String])
|
||||
@@ -27,7 +27,7 @@ enum RoomScreenViewAction {
|
||||
case tappedPinnedEventsBanner
|
||||
case viewAllPins
|
||||
case displayRoomDetails
|
||||
case displayCall
|
||||
case displayCall(isVoiceCall: Bool)
|
||||
case footerViewAction(RoomScreenFooterViewAction)
|
||||
case acceptKnock(eventID: String)
|
||||
case dismissKnockRequests
|
||||
@@ -62,6 +62,9 @@ struct RoomScreenViewState: BindableState {
|
||||
isCallingEnabled && !isParticipatingInOngoingCall // Hide the join call button when already in the call
|
||||
}
|
||||
|
||||
/// Whether the current room is a DM
|
||||
var isDirectOneToOneRoom: Bool
|
||||
|
||||
var roomThreadListEnabled = false
|
||||
var isKnockingEnabled = false
|
||||
var isKnockableRoom = false
|
||||
|
||||
@@ -76,6 +76,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
let viewState = RoomScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
|
||||
roomAvatar: roomProxy.infoPublisher.value.avatar,
|
||||
hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall,
|
||||
isDirectOneToOneRoom: roomProxy.isDirectOneToOneRoom,
|
||||
hasSuccessor: roomProxy.infoPublisher.value.successor != nil,
|
||||
roomHistorySharingState: roomHistorySharingState)
|
||||
super.init(initialViewState: appHooks.roomScreenHook.update(viewState),
|
||||
@@ -98,8 +99,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
actionsSubject.send(.displayPinnedEventsTimeline)
|
||||
case .displayRoomDetails:
|
||||
actionsSubject.send(.displayRoomDetails)
|
||||
case .displayCall:
|
||||
actionsSubject.send(.displayCall)
|
||||
case .displayCall(let isVoiceCall):
|
||||
actionsSubject.send(.displayCall(isVoiceCall: isVoiceCall))
|
||||
actionsSubject.send(.removeComposerFocus)
|
||||
analyticsService.trackInteraction(name: .MobileRoomCallButton)
|
||||
case .footerViewAction(let action):
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomCallControlsToolbar: ToolbarContent {
|
||||
let viewState: RoomScreenViewState
|
||||
let onCallTap: (_ isVoiceCall: Bool) -> Void
|
||||
|
||||
var body: some ToolbarContent {
|
||||
if viewState.hasOngoingCall {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
JoinCallButton {
|
||||
onCallTap(false)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.joinCall)
|
||||
.disabled(!viewState.canJoinCall)
|
||||
}
|
||||
} else {
|
||||
if viewState.isDirectOneToOneRoom {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button { onCallTap(true) } label: {
|
||||
CompoundIcon(\.voiceCallSolid)
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yStartVoiceCall)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.startVoiceCall)
|
||||
.disabled(!viewState.canJoinCall)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button { onCallTap(false) } label: {
|
||||
CompoundIcon(\.videoCallSolid)
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yStartVideoCall)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.startVideoCall)
|
||||
.disabled(!viewState.canJoinCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomCallControlsToolbar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 0) {
|
||||
ElementNavigationStack {
|
||||
Color.clear.toolbar { RoomCallControlsToolbar(viewState: .mock(hasOngoingCall: true)) { _ in } }
|
||||
}
|
||||
ElementNavigationStack {
|
||||
Color.clear.toolbar { RoomCallControlsToolbar(viewState: .mock(hasOngoingCall: false, isDirectOneToOneRoom: true)) { _ in } }
|
||||
}
|
||||
ElementNavigationStack {
|
||||
Color.clear.toolbar { RoomCallControlsToolbar(viewState: .mock(hasOngoingCall: false)) { _ in } }
|
||||
}
|
||||
ElementNavigationStack {
|
||||
Color.clear.toolbar { RoomCallControlsToolbar(viewState: .mock(hasOngoingCall: false, canJoinCall: false)) { _ in } }
|
||||
}
|
||||
}
|
||||
.previewDisplayName("All states")
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomScreenViewState {
|
||||
static func mock(hasOngoingCall: Bool, isDirectOneToOneRoom: Bool = false, canJoinCall: Bool = true) -> RoomScreenViewState {
|
||||
RoomScreenViewState(roomAvatar: .room(id: "mock", name: "Mock Room", avatarURL: nil),
|
||||
canJoinCall: canJoinCall,
|
||||
hasOngoingCall: hasOngoingCall, isDirectOneToOneRoom: isDirectOneToOneRoom,
|
||||
hasSuccessor: false)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Compound
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
@@ -172,10 +173,9 @@ struct RoomScreen: View {
|
||||
}
|
||||
|
||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
if context.viewState.shouldShowCallButton {
|
||||
callButton
|
||||
.disabled(!context.viewState.canJoinCall)
|
||||
if context.viewState.shouldShowCallButton {
|
||||
RoomCallControlsToolbar(viewState: context.viewState) { isVoiceCall in
|
||||
context.send(viewAction: .displayCall(isVoiceCall: isVoiceCall))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,24 +194,6 @@ struct RoomScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var callButton: some View {
|
||||
if context.viewState.hasOngoingCall {
|
||||
JoinCallButton {
|
||||
context.send(viewAction: .displayCall)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.joinCall)
|
||||
} else {
|
||||
Button {
|
||||
context.send(viewAction: .displayCall)
|
||||
} label: {
|
||||
CompoundIcon(\.videoCallSolid)
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yStartCall)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.joinCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@@ -19,7 +19,7 @@ struct UserProfileScreenCoordinatorParameters {
|
||||
|
||||
enum UserProfileScreenCoordinatorAction {
|
||||
case openDirectChat(roomID: String)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .openDirectChat(let roomID):
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
case .startCall(let roomProxy):
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy))
|
||||
case .startCall(let roomProxy, let isVoiceCall):
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
enum UserProfileScreenViewModelAction {
|
||||
case openDirectChat(roomID: String)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol)
|
||||
case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ enum UserProfileScreenViewAction {
|
||||
case displayAvatar(URL)
|
||||
case openDirectChat
|
||||
case createDirectChat
|
||||
case startCall(roomID: String)
|
||||
case startCall(roomID: String, isVoiceCall: Bool)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
openDirectChat()
|
||||
case .createDirectChat:
|
||||
Task { await createDirectChat() }
|
||||
case .startCall(let roomID):
|
||||
Task { await startCall(roomID: roomID) }
|
||||
case .startCall(let roomID, let isVoiceCall):
|
||||
Task { await startCall(roomID: roomID, isVoiceCall: isVoiceCall) }
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
@@ -144,12 +144,12 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
}
|
||||
}
|
||||
|
||||
private func startCall(roomID: String) async {
|
||||
private func startCall(roomID: String, isVoiceCall: Bool) async {
|
||||
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
showErrorIndicator()
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy))
|
||||
actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
|
||||
}
|
||||
|
||||
// MARK: User Indicators
|
||||
|
||||
@@ -66,11 +66,20 @@ struct UserProfileScreen: View {
|
||||
|
||||
if let roomID = context.viewState.dmRoomID {
|
||||
Button {
|
||||
context.send(viewAction: .startCall(roomID: roomID))
|
||||
context.send(viewAction: .startCall(roomID: roomID, isVoiceCall: true))
|
||||
} label: {
|
||||
CompoundIcon(\.voiceCall)
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yStartVoiceCall)
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
|
||||
Button {
|
||||
context.send(viewAction: .startCall(roomID: roomID, isVoiceCall: false))
|
||||
} label: {
|
||||
CompoundIcon(\.videoCall)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
.accessibilityLabel(L10n.a11yStartVideoCall)
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.commonVideo))
|
||||
}
|
||||
|
||||
if let permalink = context.viewState.permalink {
|
||||
|
||||
Reference in New Issue
Block a user