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 7434d531b2
commit b05ab3ac0b
76 changed files with 303 additions and 201 deletions
+4
View File
@@ -1245,6 +1245,7 @@
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; };
D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.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 */; };
D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.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>"; };
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>"; };
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>"; };
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>"; };
@@ -5058,6 +5060,7 @@
isa = PBXGroup;
children = (
47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */,
F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */,
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */,
4137900E28201C314C835C11 /* RoomScreenFooterView.swift */,
4552D3466B1453F287223ADA /* SwipeRightAction.swift */,
@@ -8595,6 +8598,7 @@
C7CEFC1FB0547CFC8F5C84EF /* Room.swift in Sources */,
6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */,
8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */,
D433A58BFF77B3E563FB547E /* RoomCallControlsToolbar.swift in Sources */,
F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */,
86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */,
4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */,
@@ -1576,6 +1576,7 @@
"screen_leave_space_choose_owners_action" = "Choose owners";
"screen_create_room_room_access_section_private_option_title" = "Private";
"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_signout_confirmation_dialog_submit" = "Remove this device";
"screen_signout_confirmation_dialog_title" = "Remove this device";
@@ -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:
+2
View File
@@ -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 {
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:388eb52254dce7d4dfab805e3e81f8fe265b21bbdcd28996dd2378413c5c5f70
size 240854
oid sha256:94dcd8637c8fe861c8f1b7ad06345d4a05ae4720f5915200a6db6416c090b24b
size 244479
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7828ec2ac0172fdca923db059eaf3e88e58bd3fb0e9f7ba6a92ff0072a9d27e6
size 249123
oid sha256:c66756a54147c9dd81260af807454e0309b571918d46114921df865059c9ed55
size 253255
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f816f4f7cb6c47310adb914aa68fe8665184cbf5af32687217f77d024cc7af7
size 178476
oid sha256:705f10acc59058a2dab699389be490ab00c56c45581ebf5bd9451e41fdc3b179
size 180825
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d211fb06946bb500a4bf1ac453bd961a9a690260d45ca3118f3146e9acc5255a
size 184443
oid sha256:c65f2b27d5a0a5e6aef3ba18374c5f8145817656db3b21f11d1fbe7114efc736
size 185379
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:388eb52254dce7d4dfab805e3e81f8fe265b21bbdcd28996dd2378413c5c5f70
size 240854
oid sha256:94dcd8637c8fe861c8f1b7ad06345d4a05ae4720f5915200a6db6416c090b24b
size 244479
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7828ec2ac0172fdca923db059eaf3e88e58bd3fb0e9f7ba6a92ff0072a9d27e6
size 249123
oid sha256:c66756a54147c9dd81260af807454e0309b571918d46114921df865059c9ed55
size 253255
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f816f4f7cb6c47310adb914aa68fe8665184cbf5af32687217f77d024cc7af7
size 178476
oid sha256:705f10acc59058a2dab699389be490ab00c56c45581ebf5bd9451e41fdc3b179
size 180825
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d211fb06946bb500a4bf1ac453bd961a9a690260d45ca3118f3146e9acc5255a
size 184443
oid sha256:c65f2b27d5a0a5e6aef3ba18374c5f8145817656db3b21f11d1fbe7114efc736
size 185379
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:388eb52254dce7d4dfab805e3e81f8fe265b21bbdcd28996dd2378413c5c5f70
size 240854
oid sha256:94dcd8637c8fe861c8f1b7ad06345d4a05ae4720f5915200a6db6416c090b24b
size 244479
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7828ec2ac0172fdca923db059eaf3e88e58bd3fb0e9f7ba6a92ff0072a9d27e6
size 249123
oid sha256:c66756a54147c9dd81260af807454e0309b571918d46114921df865059c9ed55
size 253255
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f816f4f7cb6c47310adb914aa68fe8665184cbf5af32687217f77d024cc7af7
size 178476
oid sha256:705f10acc59058a2dab699389be490ab00c56c45581ebf5bd9451e41fdc3b179
size 180825
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d211fb06946bb500a4bf1ac453bd961a9a690260d45ca3118f3146e9acc5255a
size 184443
oid sha256:c65f2b27d5a0a5e6aef3ba18374c5f8145817656db3b21f11d1fbe7114efc736
size 185379
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec7f29d77a5869ac14b0e153e25880695b3c8ed0f3fa0ad89cd778a4295583af
size 173136
oid sha256:907d45cc33942555e6715f3b228b4194c2fe4b638818ca0ef625aeefd57edb53
size 173934
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5652f9cda5f2360f5bb097efc5b0c3d9395367d62aa6c1b9950ef38777da96db
size 182494
oid sha256:b800ee52829d51e25acde7efff99a0ae60f84494c1602a98a544389e38b3cd8b
size 183234
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5b6e999476c524f24d9f64fa3b182072bae93b3cc8a183f1c7568496948f3f9
size 111009
oid sha256:2c9f880c01560197a75a44a435ccc8af421990372aefd4d8dee8006c7fcd6613
size 111467
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad9847cdad5e86d6bd391344e77b60b2d9978c4bcdcb2e7ce61f67871cfd152a
size 118116
oid sha256:284f25d80eeb5e9bfdc61d8bd6b63ff68a7326e8d9ae4a710436e97f0138c39c
size 117693
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f49305b619e5bd49b3eae2549a08cb7dcaa60ba488d73fcf318bf30ff2b1078
size 172149
oid sha256:b28a3f68af15043b7780d6431cdc4fa209c768ca02d5dc89d28c17b96ad2486c
size 172948
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2af5425c5f6aac5be08cd390f175a5ee2993a1430c44b9966d2bb5c950bda7ba
size 180358
oid sha256:59b7f2c35d1ed1dcbfd7e88e1bfa7cbf671ca952b42ddcb764280eec3bbc8cb6
size 181098
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9518ec10e7311513eb2fba873f82e1f783c16f9941879eb8524537cdc32a788f
size 109649
oid sha256:46d4d32b88d36b6c50a0a640e1c9a86827a2f4f4bdefc23a5adee118b7bded52
size 110112
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7d3f7ecd746501ab3049ae30c26ab68edf6d54867c99aee0297afe00e6b0aeb
size 114937
oid sha256:3509aa590d99133d1f3b2383ae1557507ca527fbbb9defb96474dd6426f58097
size 114738
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a7ae4e629548ee93e510421978e5c22423cd08ffd309fd8c0a510b6e7bb4a679
size 141005
oid sha256:e3879b9801c1e85a501c54a3aea379574687c402c5f71137edb55f0165316065
size 141852
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a0c619bcad96ad5ca115cb08f04009c090690d3fcf5d86471d8b73c0f09b8adf
size 150248
oid sha256:1ffb5e8075bd62a798a8ca564decee431c009f43a8296c09eb30354293d32a9a
size 151000
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:472b06b9e0b6d3c611225238e96f4f76752aa0286d0b03df78ed4d3fb8ff669a
size 89188
oid sha256:00d1e4b9f9d2dd835e82015a4f40369899a7ca9b2acddca6bde6000b74339c6d
size 89653
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7219e675c76d1f0fd25f436ce8715a9911aa57fef855d09ea3ad46083526b11c
size 103431
oid sha256:5094a5ef99f1ded6df8929aceea0419ae317c53edfff5e7b7883b99d7db5a82f
size 103120
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91728f0b40f1ab928bfdec79cb8b0c1339b3b9f1d8dc0fcd2f335436bd7bf8fb
size 114267
oid sha256:e55bfefa631fd86118d38548496fa5b87b7bebf5d78ea8026dbfdaab66289c4e
size 118005
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b9f4402d85c407288c48e0a717ae1f939d77bb58fa0019e80514bcc95144dbac
size 116500
oid sha256:3ec9356526a2c6b939332f0185957959560fa3ef10169896d857dcb984fd1c8e
size 120458
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fe8592fc0d441d7b3ec96bf23b6d75a934b6779a51909692ba91b8dbe7563c6d
size 65482
oid sha256:fde6a61b03d94e85bbb60deeabe058df2aacc36607595f1a9333df3dfdc9a9da
size 67757
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8099df8aea5ecaa19fcc9436b6abe282632a710070f1a78ca155fb6c554c53a
size 74229
oid sha256:5b8439047e30b22f11f66993124a0e267cd085266001641c294fc1d0cb418a04
size 78751
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a63cd9eb493d39bad76e28f824c43d7c5b66ee3bd65643569d5f9146cd78c996
size 113945
oid sha256:a6e660f69e6d6fadbcc07dd9ca237111f8d08694da97d6c052797fabb988e2a7
size 117629
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b057a5ce1f74a1e2c816e8352a2f94dd2b4fe1b583e9b6bc08a6c90a9fd7e4a1
size 115895
oid sha256:f0a40432b43145db08e6374dd87597ce07b475b889dbdb1ed4b0fac44f4fafbd
size 119795
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:059b2f9ecec173c8763227b5490dd71f6cae4e730da81565064da4c144ddece0
size 64635
oid sha256:9cfd1a0a66763efa36b513be7274e87d17b60f82f0881d9f784b3bae0e59ced7
size 66887
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3278a9d8cb52dbbfad5f3d2100abd41c86a6c65e92ca39430766dcc0d519d1de
size 73463
oid sha256:621a039afdda6282671f969e9fc2cb3d1c91c3c8b2578618329be60412cebf59
size 77994
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b8c70c07daf35f838fa4a28c620ff2fcc463b839417bb58028f6ede86fc193a
size 124297
oid sha256:3f8d1a1bd4aba19644e6c1fb4fde3d270c6b3d20ae0ed6c402dba0626f95a22a
size 127833
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56726ac6089a175ce3c075624e84db8cb10472b0b0c7ee3cd5dc7dc6e022ea13
size 131714
oid sha256:e41837d862780e3d24b9f48c6c8b967381544757b65e30e9cc513dd37725776f
size 135459
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ba4424e833d4540fd1917d66978ef46509ab03f18ff1d5c389379d1575cc71e
size 73994
oid sha256:4ef0e08373847ee4ecdad0e79a85585362f667866cb2198c0b5d05c60a70b4af
size 76352
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f3cc7d36bad2817ff6b3a574219ac06d4e991d241bc81f56433a841175ae7b6
size 91800
oid sha256:aee07f391f2d96bb54554643c6fd30659014324405ab148029a4d82f268b54fe
size 95894
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cc8cb68d0a2dfccc7f47c6d511f8b3744c03917dc422c38bd744bc551589f43
size 198828
oid sha256:9be0e22bf3e8cf44a9cfd49f7ba2dd0b30a3b17b3e95106717f1a56a14949e59
size 202563
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7dae065e97c86924e243f3043d7d0cc452d821a862c701e1f863ca45a167ca8d
size 200388
oid sha256:14559ec69b77dc0447bc05417f989fd0352963d0526ba38f361f60c89d6138b7
size 204271
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f667076bf71c4b98134d06d0b54e702bf686f0545126fe33584236018611388e
size 138130
oid sha256:ae9c8277f79aff3e8fb4f8656fbdc7fb476923c4ba15a4762ee9a1fa426999e2
size 140376
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbe079028e0d4863f751aef877732136621dc681fc385b726c5737eb538dce99
size 146185
oid sha256:37a286fc1ca7d1a3bc2fd3f2d3dc6efdc5e84536d679dafc35e828ba28d4d6ee
size 150416
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a7d058c2291c08292e754a8d40852f0059abd56a10a996cea9c98b5394df5f26
size 101600
oid sha256:7d6bd210ca7bc8eda3d6e17c441d79affcef73d61cee0bcb1fd5b64560fba71a
size 105293
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:931f72d1291176b036f0216b85aa375d8e4e775d7d3ef284e1e7eb92fef9c6c9
size 102543
oid sha256:b343f23051d1259931f52f1130761d45ed5f4f671f10722a4c35cd32b074453f
size 106419
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f09a2e4e168fda6126a7f7798e373156b7bc429f05dad095f4fb45cb05bc69c
size 53165
oid sha256:cd47ad7af169e3f7f40c6005c4d18ce499068f1537eab413f7bd84c3d0cd12f8
size 55460
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41f99689c892019e23998cba24121c3cac7ae2d1a1c14451085ea3de56388b71
size 58638
oid sha256:8e146bf35ac721bba4650865a22a7e697389b28a17ba5032da31f5bb9ac1a9b0
size 63113
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a0ed6566f014f2a866690ab9a47b21100134b889789e7301de889e3e9588b45
size 104863
oid sha256:e9216212c4e4f49eb8c7098fb8af58087268ded788ef7059081d293c5789d087
size 108595
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1dc268599b22f1ff10b17ad948299c7870ee71abda787aefe4794d538b2c763f
size 106060
oid sha256:91cc65967c09cb1be9c8fa9e4fc2ff252b74ca3694ba3df7e519dff57a66cd54
size 109858
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a5ef3acddef9ce4cbbe6c4a939cb7f0598fa3d4c8e73cc7c6df9ec9e2f00894
size 56011
oid sha256:92a3c3cc7a85087bfcfb7c1a133290c3a6cf27d65ed64dccaa193ceb5bf9607f
size 58261
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:977c64bac59e3b8c5a1438a1c04117cb346ccbb643bd02bff1e8c94559d3882e
size 62374
oid sha256:2208ae4768076d0eeac0b624db5f9e4a84e4221d8326d725e9de3c71eb981865
size 66798