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:
Valere Fedronic
2026-04-09 17:22:31 +02:00
committed by GitHub
parent d792f6e4f9
commit ec00eac164
76 changed files with 303 additions and 201 deletions

View File

@@ -1245,6 +1245,7 @@
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; }; D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; };
D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */; }; D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */; };
D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; }; D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; };
D433A58BFF77B3E563FB547E /* RoomCallControlsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */; };
D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; };
D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; };
D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; };
@@ -2954,6 +2955,7 @@
F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionMock.swift; sourceTree = "<group>"; }; F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionMock.swift; sourceTree = "<group>"; };
F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsView.swift; sourceTree = "<group>"; }; F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsView.swift; sourceTree = "<group>"; };
F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderTests.swift; sourceTree = "<group>"; }; F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderTests.swift; sourceTree = "<group>"; };
F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomCallControlsToolbar.swift; sourceTree = "<group>"; };
F4CEB4590CCF70F0E3C0B171 /* GeneratedAccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedAccessibilityTests.swift; sourceTree = "<group>"; }; F4CEB4590CCF70F0E3C0B171 /* GeneratedAccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedAccessibilityTests.swift; sourceTree = "<group>"; };
F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F51D674A5B5F1FE1B878E20F /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = "<group>"; }; F51D674A5B5F1FE1B878E20F /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -5058,6 +5060,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */, 47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */,
F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */,
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */,
4137900E28201C314C835C11 /* RoomScreenFooterView.swift */, 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */,
4552D3466B1453F287223ADA /* SwipeRightAction.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */,
@@ -8595,6 +8598,7 @@
C7CEFC1FB0547CFC8F5C84EF /* Room.swift in Sources */, C7CEFC1FB0547CFC8F5C84EF /* Room.swift in Sources */,
6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */, 6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */,
8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */, 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */,
D433A58BFF77B3E563FB547E /* RoomCallControlsToolbar.swift in Sources */,
F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */, F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */,
86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */, 86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */,
4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */, 4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */,

View File

@@ -1576,6 +1576,7 @@
"screen_leave_space_choose_owners_action" = "Choose owners"; "screen_leave_space_choose_owners_action" = "Choose owners";
"screen_create_room_room_access_section_private_option_title" = "Private"; "screen_create_room_room_access_section_private_option_title" = "Private";
"screen_security_and_privacy_room_access_space_members_option_title" = "Space members"; "screen_security_and_privacy_room_access_space_members_option_title" = "Space members";
"a11y_start_video_call" = "Start a video call";
"screen_chat_backup_recovery_action_setup" = "Get recovery key"; "screen_chat_backup_recovery_action_setup" = "Get recovery key";
"screen_signout_confirmation_dialog_submit" = "Remove this device"; "screen_signout_confirmation_dialog_submit" = "Remove this device";
"screen_signout_confirmation_dialog_title" = "Remove this device"; "screen_signout_confirmation_dialog_title" = "Remove this device";

View File

@@ -16,7 +16,7 @@ enum ChatsTabFlowCoordinatorAction {
case showSettings case showSettings
case showChatBackupSettings case showChatBackupSettings
case sessionVerification(SessionVerificationScreenFlow) case sessionVerification(SessionVerificationScreenFlow)
case showCallScreen(roomProxy: JoinedRoomProxyProtocol) case showCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case hideCallScreenOverlay case hideCallScreenOverlay
case logout case logout
} }
@@ -535,8 +535,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.showCallScreen(roomProxy: roomProxy)) actionsSubject.send(.showCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID))) actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
case .continueWithSpaceFlow(let spaceRoomListProxy): case .continueWithSpaceFlow(let spaceRoomListProxy):
@@ -597,8 +597,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
.sink { [weak self] action in .sink { [weak self] action in
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.showCallScreen(roomProxy: roomProxy)) actionsSubject.send(.showCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID))) actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
case .finished: case .finished:
@@ -800,8 +800,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
case .openDirectChat(let roomID): case .openDirectChat(let roomID):
navigationSplitCoordinator.setSheetCoordinator(nil) navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room)) stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
case .startCall(let roomProxy): case .startCall(let roomProxy, let isVoiceCall):
actionsSubject.send(.showCallScreen(roomProxy: roomProxy)) actionsSubject.send(.showCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .dismiss: case .dismiss:
navigationSplitCoordinator.setSheetCoordinator(nil) navigationSplitCoordinator.setSheetCoordinator(nil)
} }

View File

@@ -12,7 +12,7 @@ import SwiftUI
import UserNotifications import UserNotifications
enum RoomFlowCoordinatorAction: Equatable { enum RoomFlowCoordinatorAction: Equatable {
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol) case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case verifyUser(userID: String) case verifyUser(userID: String)
/// The requested room was actually a space. The room flow has been dismissed /// The requested room was actually a space. The room flow has been dismissed
/// and a space flow should be started to continue. /// and a space flow should be started to continue.
@@ -21,8 +21,8 @@ enum RoomFlowCoordinatorAction: Equatable {
static func == (lhs: RoomFlowCoordinatorAction, rhs: RoomFlowCoordinatorAction) -> Bool { static func == (lhs: RoomFlowCoordinatorAction, rhs: RoomFlowCoordinatorAction) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.presentCallScreen(let lhsRoomProxy), .presentCallScreen(let rhsRoomProxy)): case (.presentCallScreen(let lhsRoomProxy, let lhsIsVoiceCall), .presentCallScreen(let rhsRoomProxy, let rhsIsVoiceCall)):
lhsRoomProxy.id == rhsRoomProxy.id lhsRoomProxy.id == rhsRoomProxy.id && lhsIsVoiceCall == rhsIsVoiceCall
case (.finished, .finished): case (.finished, .finished):
true true
default: default:
@@ -717,7 +717,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentMessageForwarding(let forwardingItem): case .presentMessageForwarding(let forwardingItem):
stateMachine.tryEvent(.presentMessageForwarding(forwardingItem: forwardingItem)) stateMachine.tryEvent(.presentMessageForwarding(forwardingItem: forwardingItem))
case .presentCallScreen: case .presentCallScreen:
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
case .presentPinnedEventsTimeline: case .presentPinnedEventsTimeline:
stateMachine.tryEvent(.presentPinnedEventsTimeline) stateMachine.tryEvent(.presentPinnedEventsTimeline)
case .presentResolveSendFailure(failure: let failure, sendHandle: let sendHandle): case .presentResolveSendFailure(failure: let failure, sendHandle: let sendHandle):
@@ -952,8 +952,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentPollsHistory) stateMachine.tryEvent(.presentPollsHistory)
case .presentRolesAndPermissionsScreen: case .presentRolesAndPermissionsScreen:
stateMachine.tryEvent(.presentRolesAndPermissionsScreen) stateMachine.tryEvent(.presentRolesAndPermissionsScreen)
case .presentCall: case .presentCall(isVoiceCall: let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .presentPinnedEventsTimeline: case .presentPinnedEventsTimeline:
stateMachine.tryEvent(.presentPinnedEventsTimeline) stateMachine.tryEvent(.presentPinnedEventsTimeline)
case .presentKnockingRequestsListScreen: case .presentKnockingRequestsListScreen:
@@ -1505,8 +1505,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
case .continueWithSpaceFlow(let spaceRoomListProxy): case .continueWithSpaceFlow(let spaceRoomListProxy):
@@ -1615,8 +1615,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
.sink { [weak self] action in .sink { [weak self] action in
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
case .finished: case .finished:
@@ -1641,8 +1641,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .finished: case .finished:
stateMachine.tryEvent(.stopMembersFlow) stateMachine.tryEvent(.stopMembersFlow)
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
} }

View File

@@ -11,7 +11,7 @@ import SwiftUI
enum RoomMembersFlowCoordinatorAction { enum RoomMembersFlowCoordinatorAction {
case finished case finished
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol) case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case verifyUser(userID: String) case verifyUser(userID: String)
} }
@@ -235,8 +235,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentUserProfile(userID: userID)) stateMachine.tryEvent(.presentUserProfile(userID: userID))
case .openDirectChat(let roomID): case .openDirectChat(let roomID):
stateMachine.tryEvent(.startRoomFlow(roomID: roomID, via: [], eventID: nil)) stateMachine.tryEvent(.startRoomFlow(roomID: roomID, via: [], eventID: nil))
case .startCall(let roomProxy): case .startCall(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
} }
@@ -294,8 +294,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .openDirectChat(let roomID): case .openDirectChat(let roomID):
stateMachine.tryEvent(.startRoomFlow(roomID: roomID, via: [], eventID: nil)) stateMachine.tryEvent(.startRoomFlow(roomID: roomID, via: [], eventID: nil))
case .startCall(let roomProxy): case .startCall(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .dismiss: case .dismiss:
break // Not supported when pushed. break // Not supported when pushed.
} }
@@ -322,8 +322,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
case .continueWithSpaceFlow: case .continueWithSpaceFlow:

View File

@@ -11,7 +11,7 @@ import Foundation
import SwiftState import SwiftState
enum SpaceFlowCoordinatorAction { enum SpaceFlowCoordinatorAction {
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol) case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case verifyUser(userID: String) case verifyUser(userID: String)
case finished case finished
} }
@@ -504,8 +504,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
case .finished: case .finished:
@@ -529,8 +529,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
case .continueWithSpaceFlow(let spaceRoomListProxy): case .continueWithSpaceFlow(let spaceRoomListProxy):
@@ -557,8 +557,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
switch actions { switch actions {
case .finished: case .finished:
stateMachine.tryEvent(.stopMembersFlow) stateMachine.tryEvent(.stopMembersFlow)
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
} }
@@ -582,7 +582,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.leftSpace) stateMachine.tryEvent(.leftSpace)
} }
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: false))
case .verifyUser(userID: let userID): case .verifyUser(userID: let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
} }

View File

@@ -395,7 +395,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .finished: case .finished:
stateMachine.tryEvent(.stopMembersListFlow) stateMachine.tryEvent(.stopMembersListFlow)
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))

View File

@@ -12,7 +12,7 @@ import SwiftState
enum SpacesTabFlowCoordinatorAction { enum SpacesTabFlowCoordinatorAction {
case showSettings case showSettings
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol) case presentCallScreen(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case verifyUser(userID: String) case verifyUser(userID: String)
} }
@@ -178,8 +178,8 @@ class SpacesTabFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) actionsSubject.send(.presentCallScreen(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
case .finished: case .finished:

View File

@@ -207,8 +207,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
handleAppRoute(.chatBackupSettings, animated: true) handleAppRoute(.chatBackupSettings, animated: true)
case .sessionVerification(let flow): case .sessionVerification(let flow):
presentSessionVerificationScreen(flow: flow) presentSessionVerificationScreen(flow: flow)
case .showCallScreen(let roomProxy): case .showCallScreen(let roomProxy, let isVoiceCall):
presentCallScreen(roomProxy: roomProxy, voiceOnly: false) presentCallScreen(roomProxy: roomProxy, voiceOnly: isVoiceCall)
case .hideCallScreenOverlay: case .hideCallScreenOverlay:
hideCallScreenOverlay() hideCallScreenOverlay()
case .logout: case .logout:
@@ -221,8 +221,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
.sink { [weak self] action in .sink { [weak self] action in
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy, let isVoiceCall):
presentCallScreen(roomProxy: roomProxy, voiceOnly: false) presentCallScreen(roomProxy: roomProxy, voiceOnly: isVoiceCall)
case .verifyUser(let userID): case .verifyUser(let userID):
presentSessionVerificationScreen(flow: .userInitiator(userID: userID)) presentSessionVerificationScreen(flow: .userInitiator(userID: userID))
case .showSettings: case .showSettings:

View File

@@ -120,6 +120,8 @@ internal enum L10n {
internal static var a11yShowPassword: String { return L10n.tr("Localizable", "a11y_show_password") } internal static var a11yShowPassword: String { return L10n.tr("Localizable", "a11y_show_password") }
/// Start a call /// Start a call
internal static var a11yStartCall: String { return L10n.tr("Localizable", "a11y_start_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 /// Start a voice call
internal static var a11yStartVoiceCall: String { return L10n.tr("Localizable", "a11y_start_voice_call") } internal static var a11yStartVoiceCall: String { return L10n.tr("Localizable", "a11y_start_voice_call") }
/// Tombstoned room /// Tombstoned room

View File

@@ -157,6 +157,8 @@ enum A11yIdentifiers {
let attachmentPickerTextFormatting = "room-attachment_picker_text_formatting" let attachmentPickerTextFormatting = "room-attachment_picker_text_formatting"
let timelineItemActionMenu = "room-timeline_item_action_menu" let timelineItemActionMenu = "room-timeline_item_action_menu"
let joinCall = "room-join_call" let joinCall = "room-join_call"
let startVoiceCall = "room-start_voice_call"
let startVideoCall = "room-start_video_call"
let scrollToBottom = "room-scroll_to_bottom" let scrollToBottom = "room-scroll_to_bottom"
let messageComposer = "room-message_composer" let messageComposer = "room-message_composer"

View File

@@ -28,7 +28,7 @@ enum RoomDetailsScreenCoordinatorAction {
case presentInviteUsersScreen case presentInviteUsersScreen
case presentPollsHistory case presentPollsHistory
case presentRolesAndPermissionsScreen case presentRolesAndPermissionsScreen
case presentCall case presentCall(isVoiceCall: Bool)
case presentPinnedEventsTimeline case presentPinnedEventsTimeline
case presentMediaEventsTimeline case presentMediaEventsTimeline
case presentKnockingRequestsListScreen case presentKnockingRequestsListScreen
@@ -81,8 +81,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentPollsHistory) actionsSubject.send(.presentPollsHistory)
case .requestRolesAndPermissionsPresentation: case .requestRolesAndPermissionsPresentation:
actionsSubject.send(.presentRolesAndPermissionsScreen) actionsSubject.send(.presentRolesAndPermissionsScreen)
case .startCall: case .startCall(let isVoiceCall):
actionsSubject.send(.presentCall) actionsSubject.send(.presentCall(isVoiceCall: isVoiceCall))
case .displayPinnedEventsTimeline: case .displayPinnedEventsTimeline:
actionsSubject.send(.presentPinnedEventsTimeline) actionsSubject.send(.presentPinnedEventsTimeline)
case .displayMediaEventsTimeline: case .displayMediaEventsTimeline:

View File

@@ -22,7 +22,7 @@ enum RoomDetailsScreenViewModelAction: Equatable {
case requestEditDetailsPresentation case requestEditDetailsPresentation
case requestPollsHistoryPresentation case requestPollsHistoryPresentation
case requestRolesAndPermissionsPresentation case requestRolesAndPermissionsPresentation
case startCall case startCall(isVoiceCall: Bool)
case displayPinnedEventsTimeline case displayPinnedEventsTimeline
case displayMediaEventsTimeline case displayMediaEventsTimeline
case displayKnockingRequests case displayKnockingRequests
@@ -94,7 +94,10 @@ struct RoomDetailsScreenViewState: BindableState {
var shortcuts: [RoomDetailsScreenViewShortcut] { var shortcuts: [RoomDetailsScreenViewShortcut] {
var shortcuts: [RoomDetailsScreenViewShortcut] = [.mute] var shortcuts: [RoomDetailsScreenViewShortcut] = [.mute]
if !ProcessInfo.processInfo.isiOSAppOnMac, canJoinCall { if !ProcessInfo.processInfo.isiOSAppOnMac, canJoinCall {
shortcuts.append(.call) if isDirect {
shortcuts.append(.voiceCall)
}
shortcuts.append(.videoCall)
} }
if dmRecipientInfo == nil, canInviteUsers { if dmRecipientInfo == nil, canInviteUsers {
shortcuts.append(.invite) shortcuts.append(.invite)
@@ -223,7 +226,7 @@ enum RoomDetailsScreenViewAction {
case toggleFavourite(isFavourite: Bool) case toggleFavourite(isFavourite: Bool)
case processTapRolesAndPermissions case processTapRolesAndPermissions
case processTapSecurityAndPrivacy case processTapSecurityAndPrivacy
case processTapCall case processTapCall(isVoiceCall: Bool)
case processTapPinnedEvents case processTapPinnedEvents
case processTapMediaEvents case processTapMediaEvents
case processTapRequestsToJoin case processTapRequestsToJoin
@@ -233,7 +236,8 @@ enum RoomDetailsScreenViewAction {
enum RoomDetailsScreenViewShortcut { enum RoomDetailsScreenViewShortcut {
case share(link: URL) case share(link: URL)
case mute case mute
case call case videoCall
case voiceCall
case invite case invite
} }

View File

@@ -151,8 +151,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
Task { await toggleFavourite(isFavourite) } Task { await toggleFavourite(isFavourite) }
case .processTapRolesAndPermissions: case .processTapRolesAndPermissions:
actionsSubject.send(.requestRolesAndPermissionsPresentation) actionsSubject.send(.requestRolesAndPermissionsPresentation)
case .processTapCall: case .processTapCall(let isVoiceCall):
actionsSubject.send(.startCall) actionsSubject.send(.startCall(isVoiceCall: isVoiceCall))
case .processTapPinnedEvents: case .processTapPinnedEvents:
analyticsService.trackInteraction(name: .PinnedMessageRoomInfoButton) analyticsService.trackInteraction(name: .PinnedMessageRoomInfoButton)
actionsSubject.send(.displayPinnedEventsTimeline) actionsSubject.send(.displayPinnedEventsTimeline)

View File

@@ -85,13 +85,22 @@ struct RoomDetailsScreen: View {
CompoundIcon(\.shareIos) CompoundIcon(\.shareIos)
} }
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) .buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
case .call: case .voiceCall:
Button { 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: { } label: {
CompoundIcon(\.videoCall) CompoundIcon(\.videoCall)
} }
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall)) .accessibilityLabel(L10n.a11yStartVideoCall)
.buttonStyle(FormActionButtonStyle(title: L10n.commonVideo))
case .invite: case .invite:
Button { Button {
context.send(viewAction: .processTapInvite) context.send(viewAction: .processTapInvite)

View File

@@ -20,7 +20,7 @@ struct RoomMemberDetailsScreenCoordinatorParameters {
enum RoomMemberDetailsScreenCoordinatorAction { enum RoomMemberDetailsScreenCoordinatorAction {
case openUserProfile case openUserProfile
case openDirectChat(roomID: String) case openDirectChat(roomID: String)
case startCall(roomProxy: JoinedRoomProxyProtocol) case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case verifyUser(userID: String) case verifyUser(userID: String)
} }
@@ -51,8 +51,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.openUserProfile) actionsSubject.send(.openUserProfile)
case .openDirectChat(let roomID): case .openDirectChat(let roomID):
actionsSubject.send(.openDirectChat(roomID: roomID)) actionsSubject.send(.openDirectChat(roomID: roomID))
case .startCall(let roomProxy): case .startCall(let roomProxy, let isVoiceCall):
actionsSubject.send(.startCall(roomProxy: roomProxy)) actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .verifyUser(let userID): case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID)) actionsSubject.send(.verifyUser(userID: userID))
} }

View File

@@ -11,7 +11,7 @@ import Foundation
enum RoomMemberDetailsScreenViewModelAction { enum RoomMemberDetailsScreenViewModelAction {
case openUserProfile case openUserProfile
case openDirectChat(roomID: String) case openDirectChat(roomID: String)
case startCall(roomProxy: JoinedRoomProxyProtocol) case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case verifyUser(userID: String) case verifyUser(userID: String)
} }
@@ -93,7 +93,7 @@ enum RoomMemberDetailsScreenViewAction {
case displayAvatar(URL) case displayAvatar(URL)
case openDirectChat case openDirectChat
case createDirectChat case createDirectChat
case startCall(roomID: String) case startCall(roomID: String, isVoiceCall: Bool)
case verifyUser case verifyUser
case withdrawVerification case withdrawVerification
} }

View File

@@ -81,8 +81,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
openDirectChat() openDirectChat()
case .createDirectChat: case .createDirectChat:
Task { await createDirectChat() } Task { await createDirectChat() }
case .startCall(let roomID): case .startCall(let roomID, let isVoiceCall):
Task { await startCall(roomID: roomID) } Task { await startCall(roomID: roomID, isVoiceCall: isVoiceCall) }
case .verifyUser: case .verifyUser:
actionsSubject.send(.verifyUser(userID: state.userID)) actionsSubject.send(.verifyUser(userID: state.userID))
case .withdrawVerification: 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 { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
showErrorIndicator() showErrorIndicator()
return return
} }
actionsSubject.send(.startCall(roomProxy: roomProxy)) actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
} }
// MARK: User Indicators // MARK: User Indicators

View File

@@ -103,11 +103,20 @@ struct RoomMemberDetailsScreen: View {
if let roomID = context.viewState.dmRoomID { if let roomID = context.viewState.dmRoomID {
Button { 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: { } label: {
CompoundIcon(\.videoCall) CompoundIcon(\.videoCall)
} }
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall)) .accessibilityLabel(L10n.a11yStartVideoCall)
.buttonStyle(FormActionButtonStyle(title: L10n.commonVideo))
} }
if let permalink = context.viewState.memberDetails?.permalink { if let permalink = context.viewState.memberDetails?.permalink {

View File

@@ -43,7 +43,7 @@ enum RoomScreenCoordinatorAction {
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>) case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case presentRoomMemberDetails(userID: String) case presentRoomMemberDetails(userID: String)
case presentMessageForwarding(forwardingItem: MessageForwardingItem) case presentMessageForwarding(forwardingItem: MessageForwardingItem)
case presentCallScreen case presentCallScreen(isVoiceCall: Bool)
case presentPinnedEventsTimeline case presentPinnedEventsTimeline
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case presentKnockRequestsList case presentKnockRequestsList
@@ -178,8 +178,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentPinnedEventsTimeline) actionsSubject.send(.presentPinnedEventsTimeline)
case .displayRoomDetails: case .displayRoomDetails:
actionsSubject.send(.presentRoomDetails) actionsSubject.send(.presentRoomDetails)
case .displayCall: case .displayCall(let isVoiceCall):
actionsSubject.send(.presentCallScreen) actionsSubject.send(.presentCallScreen(isVoiceCall: isVoiceCall))
case .removeComposerFocus: case .removeComposerFocus:
composerViewModel.process(timelineAction: .removeFocus) composerViewModel.process(timelineAction: .removeFocus)
case .displayKnockRequests: case .displayKnockRequests:

View File

@@ -16,7 +16,7 @@ enum RoomScreenViewModelAction: Equatable {
case displayThread(threadRootEventID: String, focussedEventID: String) case displayThread(threadRootEventID: String, focussedEventID: String)
case displayPinnedEventsTimeline case displayPinnedEventsTimeline
case displayRoomDetails case displayRoomDetails
case displayCall case displayCall(isVoiceCall: Bool)
case removeComposerFocus case removeComposerFocus
case displayKnockRequests case displayKnockRequests
case displayRoom(roomID: String, via: [String]) case displayRoom(roomID: String, via: [String])
@@ -27,7 +27,7 @@ enum RoomScreenViewAction {
case tappedPinnedEventsBanner case tappedPinnedEventsBanner
case viewAllPins case viewAllPins
case displayRoomDetails case displayRoomDetails
case displayCall case displayCall(isVoiceCall: Bool)
case footerViewAction(RoomScreenFooterViewAction) case footerViewAction(RoomScreenFooterViewAction)
case acceptKnock(eventID: String) case acceptKnock(eventID: String)
case dismissKnockRequests case dismissKnockRequests
@@ -62,6 +62,9 @@ struct RoomScreenViewState: BindableState {
isCallingEnabled && !isParticipatingInOngoingCall // Hide the join call button when already in the call 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 roomThreadListEnabled = false
var isKnockingEnabled = false var isKnockingEnabled = false
var isKnockableRoom = false var isKnockableRoom = false

View File

@@ -76,6 +76,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
let viewState = RoomScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id, let viewState = RoomScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
roomAvatar: roomProxy.infoPublisher.value.avatar, roomAvatar: roomProxy.infoPublisher.value.avatar,
hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall, hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall,
isDirectOneToOneRoom: roomProxy.isDirectOneToOneRoom,
hasSuccessor: roomProxy.infoPublisher.value.successor != nil, hasSuccessor: roomProxy.infoPublisher.value.successor != nil,
roomHistorySharingState: roomHistorySharingState) roomHistorySharingState: roomHistorySharingState)
super.init(initialViewState: appHooks.roomScreenHook.update(viewState), super.init(initialViewState: appHooks.roomScreenHook.update(viewState),
@@ -98,8 +99,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
actionsSubject.send(.displayPinnedEventsTimeline) actionsSubject.send(.displayPinnedEventsTimeline)
case .displayRoomDetails: case .displayRoomDetails:
actionsSubject.send(.displayRoomDetails) actionsSubject.send(.displayRoomDetails)
case .displayCall: case .displayCall(let isVoiceCall):
actionsSubject.send(.displayCall) actionsSubject.send(.displayCall(isVoiceCall: isVoiceCall))
actionsSubject.send(.removeComposerFocus) actionsSubject.send(.removeComposerFocus)
analyticsService.trackInteraction(name: .MobileRoomCallButton) analyticsService.trackInteraction(name: .MobileRoomCallButton)
case .footerViewAction(let action): case .footerViewAction(let action):

View File

@@ -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)
}
}

View File

@@ -6,6 +6,7 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import Combine
import Compound import Compound
import SwiftUI import SwiftUI
import WysiwygComposer import WysiwygComposer
@@ -172,10 +173,9 @@ struct RoomScreen: View {
} }
if !ProcessInfo.processInfo.isiOSAppOnMac { if !ProcessInfo.processInfo.isiOSAppOnMac {
ToolbarItem(placement: .primaryAction) { if context.viewState.shouldShowCallButton {
if context.viewState.shouldShowCallButton { RoomCallControlsToolbar(viewState: context.viewState) { isVoiceCall in
callButton context.send(viewAction: .displayCall(isVoiceCall: isVoiceCall))
.disabled(!context.viewState.canJoinCall)
} }
} }
} }
@@ -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 // MARK: - Previews

View File

@@ -19,7 +19,7 @@ struct UserProfileScreenCoordinatorParameters {
enum UserProfileScreenCoordinatorAction { enum UserProfileScreenCoordinatorAction {
case openDirectChat(roomID: String) case openDirectChat(roomID: String)
case startCall(roomProxy: JoinedRoomProxyProtocol) case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case dismiss case dismiss
} }
@@ -48,8 +48,8 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
switch action { switch action {
case .openDirectChat(let roomID): case .openDirectChat(let roomID):
actionsSubject.send(.openDirectChat(roomID: roomID)) actionsSubject.send(.openDirectChat(roomID: roomID))
case .startCall(let roomProxy): case .startCall(let roomProxy, let isVoiceCall):
actionsSubject.send(.startCall(roomProxy: roomProxy)) actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }

View File

@@ -10,7 +10,7 @@ import Foundation
enum UserProfileScreenViewModelAction { enum UserProfileScreenViewModelAction {
case openDirectChat(roomID: String) case openDirectChat(roomID: String)
case startCall(roomProxy: JoinedRoomProxyProtocol) case startCall(roomProxy: JoinedRoomProxyProtocol, isVoiceCall: Bool)
case dismiss case dismiss
} }
@@ -43,7 +43,7 @@ enum UserProfileScreenViewAction {
case displayAvatar(URL) case displayAvatar(URL)
case openDirectChat case openDirectChat
case createDirectChat case createDirectChat
case startCall(roomID: String) case startCall(roomID: String, isVoiceCall: Bool)
case dismiss case dismiss
} }

View File

@@ -62,8 +62,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
openDirectChat() openDirectChat()
case .createDirectChat: case .createDirectChat:
Task { await createDirectChat() } Task { await createDirectChat() }
case .startCall(let roomID): case .startCall(let roomID, let isVoiceCall):
Task { await startCall(roomID: roomID) } Task { await startCall(roomID: roomID, isVoiceCall: isVoiceCall) }
case .dismiss: case .dismiss:
actionsSubject.send(.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 { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
showErrorIndicator() showErrorIndicator()
return return
} }
actionsSubject.send(.startCall(roomProxy: roomProxy)) actionsSubject.send(.startCall(roomProxy: roomProxy, isVoiceCall: isVoiceCall))
} }
// MARK: User Indicators // MARK: User Indicators

View File

@@ -66,11 +66,20 @@ struct UserProfileScreen: View {
if let roomID = context.viewState.dmRoomID { if let roomID = context.viewState.dmRoomID {
Button { 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: { } label: {
CompoundIcon(\.videoCall) CompoundIcon(\.videoCall)
} }
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall)) .accessibilityLabel(L10n.a11yStartVideoCall)
.buttonStyle(FormActionButtonStyle(title: L10n.commonVideo))
} }
if let permalink = context.viewState.permalink { if let permalink = context.viewState.permalink {