mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user