mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add call icon next to members in call (#5941)
* feat: add call icon next to members in call * feat: use matrix rtc room status for in-call * chore: revert xcodeproj change * chore: update preview tests * chore: Update ElementX.xcscheme * chore: undo room member status proxy --------- Co-authored-by: Kai <nankeen@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,7 @@ struct JoinedRoomProxyMockConfiguration {
|
|||||||
var isSpace = false
|
var isSpace = false
|
||||||
var isEncrypted = true
|
var isEncrypted = true
|
||||||
var hasOngoingCall = true
|
var hasOngoingCall = true
|
||||||
|
var activeRoomCallParticipants: [String] = []
|
||||||
var canonicalAlias: String?
|
var canonicalAlias: String?
|
||||||
var alternativeAliases: [String] = []
|
var alternativeAliases: [String] = []
|
||||||
var pinnedEventIDs: Set<String> = []
|
var pinnedEventIDs: Set<String> = []
|
||||||
@@ -163,7 +164,7 @@ extension RoomInfoProxyMock {
|
|||||||
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
|
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
|
||||||
isDM = isDirect && activeMembersCount <= 2
|
isDM = isDirect && activeMembersCount <= 2
|
||||||
hasRoomCall = configuration.hasOngoingCall
|
hasRoomCall = configuration.hasOngoingCall
|
||||||
activeRoomCallParticipants = []
|
activeRoomCallParticipants = configuration.activeRoomCallParticipants
|
||||||
pinnedEventIDs = configuration.pinnedEventIDs
|
pinnedEventIDs = configuration.pinnedEventIDs
|
||||||
joinRule = configuration.joinRule
|
joinRule = configuration.joinRule
|
||||||
historyVisibility = configuration.historyVisibility
|
historyVisibility = configuration.historyVisibility
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ enum RoomMembersListScreenMode {
|
|||||||
nonisolated struct RoomMemberListScreenEntry: Equatable {
|
nonisolated struct RoomMemberListScreenEntry: Equatable {
|
||||||
let member: RoomMemberDetails
|
let member: RoomMemberDetails
|
||||||
let verificationState: UserIdentityVerificationState
|
let verificationState: UserIdentityVerificationState
|
||||||
|
/// Whether the member is currently joined to the room's active MatrixRTC call (e.g. Element Call).
|
||||||
|
var isInCall = false
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomMembersListScreenViewState: BindableState {
|
struct RoomMembersListScreenViewState: BindableState {
|
||||||
|
|||||||
@@ -125,8 +125,9 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
|
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
|
||||||
// We don't care about identity statuses on non-encrypted rooms
|
// We don't care about identity statuses on non-encrypted rooms
|
||||||
let isEncrypted = roomProxy.infoPublisher.value.isEncrypted
|
let isEncrypted = roomProxy.infoPublisher.value.isEncrypted
|
||||||
|
let activeRoomCallParticipants = Set(roomProxy.infoPublisher.value.activeRoomCallParticipants)
|
||||||
|
|
||||||
return await Task.detached { [weak self] in
|
let membersDetails: RoomMembersDetails = await Task.detached { [weak self] in
|
||||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||||
var invitedMembers: [RoomMemberListScreenEntry] = .init()
|
var invitedMembers: [RoomMemberListScreenEntry] = .init()
|
||||||
var joinedMembers: [RoomMemberListScreenEntry] = .init()
|
var joinedMembers: [RoomMemberListScreenEntry] = .init()
|
||||||
@@ -137,14 +138,15 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
if isEncrypted, let fetchedState = await self?.userIdentityVerificationState(for: member.userID) {
|
if isEncrypted, let fetchedState = await self?.userIdentityVerificationState(for: member.userID) {
|
||||||
verificationState = fetchedState
|
verificationState = fetchedState
|
||||||
}
|
}
|
||||||
|
let isInCall = activeRoomCallParticipants.contains(member.userID)
|
||||||
|
|
||||||
switch member.membership {
|
switch member.membership {
|
||||||
case .invite:
|
case .invite:
|
||||||
invitedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
|
invitedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState, isInCall: isInCall))
|
||||||
case .join:
|
case .join:
|
||||||
joinedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
|
joinedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState, isInCall: isInCall))
|
||||||
case .ban:
|
case .ban:
|
||||||
bannedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
|
bannedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState, isInCall: isInCall))
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -155,6 +157,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
bannedMembers: bannedMembers.sorted { $0.member.id.localizedStandardCompare($1.member.id) == .orderedAscending }) // Re-sort ignoring display name.
|
bannedMembers: bannedMembers.sorted { $0.member.id.localizedStandardCompare($1.member.id) == .orderedAscending }) // Re-sort ignoring display name.
|
||||||
}
|
}
|
||||||
.value
|
.value
|
||||||
|
|
||||||
|
return .init(invitedMembers: membersDetails.invitedMembers,
|
||||||
|
joinedMembers: membersDetails.joinedMembers.filter(\.isInCall) + membersDetails.joinedMembers.filter { !$0.isInCall },
|
||||||
|
bannedMembers: membersDetails.bannedMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The client proxy isn't Sendable, fetch identities through this helper so
|
/// The client proxy isn't Sendable, fetch identities through this helper so
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
static let adminViewModel = makeViewModel(isAdmin: true, initialMode: .members)
|
static let adminViewModel = makeViewModel(isAdmin: true, initialMode: .members)
|
||||||
static let bannedViewModel = makeViewModel(isAdmin: true, initialMode: .banned)
|
static let bannedViewModel = makeViewModel(isAdmin: true, initialMode: .banned)
|
||||||
static let emptyBannedViewModel = makeViewModel(withBanned: false, isAdmin: false, initialMode: .members)
|
static let emptyBannedViewModel = makeViewModel(withBanned: false, isAdmin: false, initialMode: .members)
|
||||||
|
static let adminInCallViewModel = makeViewModel(withInCall: true, isAdmin: true)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ElementNavigationStack {
|
ElementNavigationStack {
|
||||||
@@ -205,6 +206,14 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
})
|
})
|
||||||
.previewDisplayName("Admin: Banned")
|
.previewDisplayName("Admin: Banned")
|
||||||
|
|
||||||
|
ElementNavigationStack {
|
||||||
|
RoomMembersListScreen(context: adminInCallViewModel.context)
|
||||||
|
}
|
||||||
|
.snapshotPreferences(expect: adminInCallViewModel.context.$viewState.map { state in
|
||||||
|
state.canBanUsers == true
|
||||||
|
})
|
||||||
|
.previewDisplayName("Admin: In-Call")
|
||||||
|
|
||||||
ElementNavigationStack {
|
ElementNavigationStack {
|
||||||
RoomMembersListScreen(context: emptyBannedViewModel.context)
|
RoomMembersListScreen(context: emptyBannedViewModel.context)
|
||||||
.onAppear { emptyBannedViewModel.context.searchQuery = "Dan" }
|
.onAppear { emptyBannedViewModel.context.searchQuery = "Dan" }
|
||||||
@@ -215,6 +224,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
static func makeViewModel(withInvites: Bool = false,
|
static func makeViewModel(withInvites: Bool = false,
|
||||||
withBanned: Bool = true,
|
withBanned: Bool = true,
|
||||||
|
withInCall: Bool = false,
|
||||||
isAdmin: Bool = false,
|
isAdmin: Bool = false,
|
||||||
initialMode: RoomMembersListScreenMode = .members) -> RoomMembersListScreenViewModel {
|
initialMode: RoomMembersListScreenMode = .members) -> RoomMembersListScreenViewModel {
|
||||||
let mockAdmin = RoomMemberProxyMock.mockAdmin
|
let mockAdmin = RoomMemberProxyMock.mockAdmin
|
||||||
@@ -228,7 +238,8 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
mockAdmin,
|
mockAdmin,
|
||||||
.mockCreator,
|
.mockCreator,
|
||||||
.mockOwner,
|
.mockOwner,
|
||||||
.mockModerator
|
.mockModerator,
|
||||||
|
.mockFrank
|
||||||
]
|
]
|
||||||
|
|
||||||
if withBanned {
|
if withBanned {
|
||||||
@@ -239,6 +250,10 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
members.append(.mockInvited)
|
members.append(.mockInvited)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let activeRoomCallParticipants = withInCall ? [RoomMemberProxyMock.mockAlice.userID,
|
||||||
|
RoomMemberProxyMock.mockOwner.userID,
|
||||||
|
mockAdmin.userID] : []
|
||||||
|
|
||||||
let clientProxyMock = ClientProxyMock(.init())
|
let clientProxyMock = ClientProxyMock(.init())
|
||||||
clientProxyMock.userIdentityForFallBackToServerClosure = { userID, _ in
|
clientProxyMock.userIdentityForFallBackToServerClosure = { userID, _ in
|
||||||
let identity = switch userID {
|
let identity = switch userID {
|
||||||
@@ -256,6 +271,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
return RoomMembersListScreenViewModel(initialMode: initialMode,
|
return RoomMembersListScreenViewModel(initialMode: initialMode,
|
||||||
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
||||||
roomProxy: JoinedRoomProxyMock(.init(name: "Some room",
|
roomProxy: JoinedRoomProxyMock(.init(name: "Some room",
|
||||||
|
activeRoomCallParticipants: activeRoomCallParticipants,
|
||||||
members: members,
|
members: members,
|
||||||
ownUserID: ownUserID,
|
ownUserID: ownUserID,
|
||||||
powerLevelsConfiguration: .init(canUserInvite: false))),
|
powerLevelsConfiguration: .init(canUserInvite: false))),
|
||||||
|
|||||||
+9
@@ -55,6 +55,12 @@ struct RoomMembersListScreenMemberCell: View {
|
|||||||
.font(.compound.bodyLG)
|
.font(.compound.bodyLG)
|
||||||
.foregroundStyle(.compound.textSecondary)
|
.foregroundStyle(.compound.textSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if listEntry.isInCall {
|
||||||
|
CompoundIcon(\.videoCallSolid, size: .small, relativeTo: .compound.bodyLG)
|
||||||
|
.foregroundStyle(.compound.iconAccentPrimary)
|
||||||
|
.accessibilityLabel(L10n.commonOnACall)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottom) {
|
.overlay(alignment: .bottom) {
|
||||||
if !isLast {
|
if !isLast {
|
||||||
@@ -117,6 +123,9 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
|
|||||||
verificationState: .verified),
|
verificationState: .verified),
|
||||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockModerator),
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockModerator),
|
||||||
verificationState: .verificationViolation),
|
verificationState: .verificationViolation),
|
||||||
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockFrank),
|
||||||
|
verificationState: .notVerified,
|
||||||
|
isInCall: true),
|
||||||
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@nodisplayname:matrix.org",
|
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@nodisplayname:matrix.org",
|
||||||
membership: .join))),
|
membership: .join))),
|
||||||
verificationState: .notVerified),
|
verificationState: .notVerified),
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:18395ebf158fcb6a95c2cb872dfe10c46524c1da11e1dde7606d641d3595dbec
|
||||||
|
size 168947
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8baf2a6430be90309622852137282670f80acb65c134b954be2f15a68c26db72
|
||||||
|
size 176958
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bf46ae15f2852960aed8d453219b03082bcc655c076946e66471e16c55b474fd
|
||||||
|
size 113640
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a83fb0e73446c7f567cf320974677ca744b101bdd0d18854d963f4f970329bfd
|
||||||
|
size 124639
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5a562486535b300d53c6a0ac094139f038bb0609895d632fde87386899ae8249
|
||||||
|
size 173785
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a5a6e0823d9f3a90987023ae393e54fc53fe7017d99f81d77630366e3ace0b9f
|
||||||
|
size 181515
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f08d918e6586257f3cb9d59ed5d1977cec27542989453f8761da76c814432946
|
||||||
|
size 119267
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2d4f8cce7e0469dd7ccebddbc8cd1964f71d5e70afd73e06f81ff70b6cc10ec5
|
||||||
|
size 129563
|
||||||
@@ -54,6 +54,30 @@ struct RoomMembersListScreenViewModelTests {
|
|||||||
#expect(viewModel.state.visibleJoinedMembers == sortedMembers)
|
#expect(viewModel.state.visibleJoinedMembers == sortedMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func sortingCallParticipants() async throws {
|
||||||
|
setup(members: [.mockAdmin, .mockFrank, .mockAlice],
|
||||||
|
activeRoomCallParticipants: [RoomMemberProxyMock.mockFrank.userID])
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.visibleJoinedMembers.count == 3
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
// Frank is in the call, so he should be sorted to the top despite having the lowest power level.
|
||||||
|
let sortedMembers: [RoomMemberListScreenEntry] = [
|
||||||
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockFrank),
|
||||||
|
verificationState: .notVerified,
|
||||||
|
isInCall: true),
|
||||||
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
|
||||||
|
verificationState: .notVerified),
|
||||||
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockAlice),
|
||||||
|
verificationState: .notVerified)
|
||||||
|
]
|
||||||
|
|
||||||
|
#expect(viewModel.state.visibleJoinedMembers == sortedMembers)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func search() async throws {
|
mutating func search() async throws {
|
||||||
setup(members: [.mockAlice, .mockBob])
|
setup(members: [.mockAlice, .mockBob])
|
||||||
@@ -293,8 +317,8 @@ struct RoomMembersListScreenViewModelTests {
|
|||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private mutating func setup(members: [RoomMemberProxyMock]) {
|
private mutating func setup(members: [RoomMemberProxyMock], activeRoomCallParticipants: [String] = []) {
|
||||||
roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members))
|
roomProxy = JoinedRoomProxyMock(.init(name: "test", activeRoomCallParticipants: activeRoomCallParticipants, members: members))
|
||||||
viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()),
|
viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()),
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
|||||||
Reference in New Issue
Block a user