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:
@@ -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)
|
||||
|
||||
+3
-3
@@ -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))
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+11
-2
@@ -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