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:
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user