mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Hide the unread dot after previewing an invite. (#3800)
* Hide the unread dot when previewing an invite. * Remove an invited room ID when accepting/rejecting. * Remove the unread badge from knocked room cells. * Update snapshots. * Address PR comments. Refactor KnockRequestType to JoinRequestType.
This commit is contained in:
parent
8c07ee35c4
commit
d325adb4fc
@ -20,6 +20,7 @@ protocol CommonSettingsProtocol {
|
||||
final class AppSettings {
|
||||
private enum UserDefaultsKeys: String {
|
||||
case lastVersionLaunched
|
||||
case seenInvites
|
||||
case appLockNumberOfPINAttempts
|
||||
case appLockNumberOfBiometricAttempts
|
||||
case timelineStyle
|
||||
@ -104,6 +105,11 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.lastVersionLaunched, storageType: .userDefaults(store))
|
||||
var lastVersionLaunched: String?
|
||||
|
||||
/// The Set of room identifiers of invites that the user already saw in the invites list.
|
||||
/// This Set is being used to implement badges for unread invites.
|
||||
@UserPreference(key: UserDefaultsKeys.seenInvites, defaultValue: [], storageType: .userDefaults(store))
|
||||
var seenInvites: Set<String>
|
||||
|
||||
/// The default homeserver address used. This is intentionally a string without a scheme
|
||||
/// so that it can be passed to Rust as a ServerName for well-known discovery.
|
||||
private(set) var defaultHomeserverAddress = "matrix.org"
|
||||
|
@ -25,6 +25,8 @@ extension InvitedRoomProxyMock {
|
||||
id = configuration.id
|
||||
inviter = configuration.inviter
|
||||
info = RoomInfoProxy(roomInfo: .init(configuration))
|
||||
|
||||
rejectInvitationReturnValue = .success(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ extension Array where Element == RoomSummary {
|
||||
static let mockRooms: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "1",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation 🔭🪐🌌",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@ -88,7 +88,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "2",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation and Empire",
|
||||
isDirect: false,
|
||||
avatarURL: .mockMXCAvatar,
|
||||
@ -105,7 +105,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "3",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Second Foundation",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@ -122,7 +122,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "4",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation's Edge",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@ -139,7 +139,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "5",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation and Earth",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@ -156,7 +156,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "6",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Prelude to Foundation",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@ -173,7 +173,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "0",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Unknown",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@ -223,7 +223,7 @@ extension Array where Element == RoomSummary {
|
||||
static let mockInvites: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "someAwesomeRoomId1",
|
||||
knockRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
name: "First room",
|
||||
isDirect: false,
|
||||
avatarURL: .mockMXCAvatar,
|
||||
@ -240,7 +240,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "someAwesomeRoomId2",
|
||||
knockRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
|
@ -9,7 +9,7 @@ import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum HomeScreenViewModelAction {
|
||||
enum HomeScreenViewModelAction: Equatable {
|
||||
case presentRoom(roomIdentifier: String)
|
||||
case presentRoomDetails(roomIdentifier: String)
|
||||
case roomLeft(roomIdentifier: String)
|
||||
@ -207,24 +207,25 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
extension HomeScreenRoom {
|
||||
init(summary: RoomSummary, hideUnreadMessagesBadge: Bool) {
|
||||
let identifier = summary.id
|
||||
init(summary: RoomSummary, hideUnreadMessagesBadge: Bool, seenInvites: Set<String> = []) {
|
||||
let roomID = summary.id
|
||||
|
||||
let hasUnreadMessages = hideUnreadMessagesBadge ? false : summary.hasUnreadMessages
|
||||
let isUnseenInvite = summary.joinRequestType?.isInvite == true && !seenInvites.contains(roomID)
|
||||
|
||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || summary.knockRequestType?.isKnock == true
|
||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || isUnseenInvite
|
||||
let isMentionShown = summary.hasUnreadMentions && !summary.isMuted
|
||||
let isMuteShown = summary.isMuted
|
||||
let isCallShown = summary.hasOngoingCall
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || summary.knockRequestType?.isKnock == true
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || isUnseenInvite
|
||||
|
||||
let type: HomeScreenRoom.RoomType = switch summary.knockRequestType {
|
||||
let type: HomeScreenRoom.RoomType = switch summary.joinRequestType {
|
||||
case .invite(let inviter): .invite(inviterDetails: inviter.map(RoomInviterDetails.init))
|
||||
case .knock: .knock
|
||||
case .none: .room
|
||||
}
|
||||
|
||||
self.init(id: identifier,
|
||||
self.init(id: roomID,
|
||||
roomID: summary.id,
|
||||
type: type,
|
||||
badges: .init(isDotShown: isDotShown,
|
||||
|
@ -98,6 +98,13 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
.weakAssign(to: \.state.isRoomDirectorySearchEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$seenInvites
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] _ in
|
||||
self?.updateRooms()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused)
|
||||
let searchQuery = context.$viewState.map(\.bindings.searchQuery)
|
||||
let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters)
|
||||
@ -290,9 +297,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
var rooms = [HomeScreenRoom]()
|
||||
let seenInvites = appSettings.seenInvites
|
||||
|
||||
for summary in roomSummaryProvider.roomListPublisher.value {
|
||||
let room = HomeScreenRoom(summary: summary, hideUnreadMessagesBadge: appSettings.hideUnreadMessagesBadge)
|
||||
let room = HomeScreenRoom(summary: summary,
|
||||
hideUnreadMessagesBadge: appSettings.hideUnreadMessagesBadge,
|
||||
seenInvites: seenInvites)
|
||||
rooms.append(room)
|
||||
}
|
||||
|
||||
@ -396,6 +406,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect,
|
||||
isSpace: roomProxy.info.isSpace,
|
||||
activeMemberCount: UInt(roomProxy.info.activeMembersCount))
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
case .failure:
|
||||
displayError()
|
||||
}
|
||||
@ -414,8 +425,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
state.bindings.alertInfo = .init(id: UUID(),
|
||||
title: title,
|
||||
message: message,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.actionDecline, role: .destructive) { Task { await self.declineInvite(roomID: room.id) } })
|
||||
primaryButton: .init(title: L10n.actionDecline, role: .destructive) { Task { await self.declineInvite(roomID: room.id) } },
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
}
|
||||
|
||||
private func declineInvite(roomID: String) async {
|
||||
@ -432,7 +443,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
let result = await roomProxy.rejectInvitation()
|
||||
|
||||
if case .failure = result {
|
||||
switch result {
|
||||
case .success:
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
case .failure:
|
||||
displayError()
|
||||
}
|
||||
}
|
||||
|
@ -122,10 +122,13 @@ struct HomeScreenInviteCell: View {
|
||||
room.isDirect ? room.inviter?.id : room.canonicalAlias
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var badge: some View {
|
||||
if room.badges.isDotShown {
|
||||
Circle()
|
||||
.scaledFrame(size: 12)
|
||||
.foregroundColor(.compound.iconAccentTertiary)
|
||||
.foregroundColor(.compound.iconAccentTertiary) // The badge is always green, no need to check isHighlighted here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,7 +181,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Some Guy",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@ -205,7 +208,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: avatarURL,
|
||||
|
@ -49,10 +49,9 @@ struct HomeScreenKnockedCell: View {
|
||||
private var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
textualContent
|
||||
badge
|
||||
}
|
||||
|
||||
// No badge - the user initiated the knock, it cannot be unread.
|
||||
|
||||
Text(L10n.screenRoomlistKnockEventSentDescription)
|
||||
.font(.compound.bodyMD)
|
||||
@ -95,12 +94,6 @@ struct HomeScreenKnockedCell: View {
|
||||
private var subtitle: String? {
|
||||
room.canonicalAlias
|
||||
}
|
||||
|
||||
private var badge: some View {
|
||||
Circle()
|
||||
.scaledFrame(size: 12)
|
||||
.foregroundColor(.compound.iconAccentTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview {
|
||||
@ -152,7 +145,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Some Guy",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@ -179,7 +172,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: avatarURL,
|
||||
|
@ -41,6 +41,18 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
|
||||
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), mediaProvider: mediaProvider)
|
||||
|
||||
context.$viewState.map(\.mode)
|
||||
.removeDuplicates()
|
||||
.sink { mode in
|
||||
switch mode {
|
||||
case .invited:
|
||||
appSettings.seenInvites.insert(roomID)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
await loadRoomDetails()
|
||||
}
|
||||
@ -225,6 +237,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
if let alias = state.roomDetails?.canonicalAlias {
|
||||
switch await clientProxy.joinRoomAlias(alias) {
|
||||
case .success:
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
actionsSubject.send(.joined)
|
||||
case .failure(let error):
|
||||
if case .forbiddenAccess = error {
|
||||
@ -238,6 +251,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
} else {
|
||||
switch await clientProxy.joinRoom(roomID, via: via) {
|
||||
case .success:
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
actionsSubject.send(.joined)
|
||||
case .failure(let error):
|
||||
if case .forbiddenAccess = error {
|
||||
@ -343,6 +357,8 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
return false
|
||||
}
|
||||
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
|
||||
actionsSubject.send(.dismiss)
|
||||
return true
|
||||
}
|
||||
|
@ -9,23 +9,21 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct RoomSummary {
|
||||
enum KnockRequestType {
|
||||
enum JoinRequestType {
|
||||
case invite(inviter: RoomMemberProxyProtocol?)
|
||||
case knock
|
||||
|
||||
var isInvite: Bool {
|
||||
if case .invite = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
switch self {
|
||||
case .invite: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var isKnock: Bool {
|
||||
if case .knock = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
switch self {
|
||||
case .knock: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,7 +32,7 @@ struct RoomSummary {
|
||||
|
||||
let id: String
|
||||
|
||||
let knockRequestType: KnockRequestType?
|
||||
let joinRequestType: JoinRequestType?
|
||||
|
||||
let name: String
|
||||
let isDirect: Bool
|
||||
@ -103,7 +101,7 @@ extension RoomSummary {
|
||||
canonicalAlias = nil
|
||||
hasOngoingCall = false
|
||||
|
||||
knockRequestType = nil
|
||||
joinRequestType = nil
|
||||
isMarkedUnread = false
|
||||
isFavourite = false
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
||||
|
||||
let knockRequestType: RoomSummary.KnockRequestType? = switch roomInfo.membership {
|
||||
let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership {
|
||||
case .invited: .invite(inviter: inviterProxy)
|
||||
case .knocked: .knock
|
||||
default: nil
|
||||
@ -263,7 +263,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
return RoomSummary(roomListItem: roomListItem,
|
||||
id: roomInfo.id,
|
||||
knockRequestType: knockRequestType,
|
||||
joinRequestType: joinRequestType,
|
||||
name: roomInfo.displayName ?? roomInfo.id,
|
||||
isDirect: roomInfo.isDirect,
|
||||
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_homeScreenKnockedCell-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
@ -22,7 +22,7 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
hasOngoingCall: Bool) {
|
||||
roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
|
||||
id: "Test room",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Test room",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
|
@ -13,13 +13,20 @@ import XCTest
|
||||
@MainActor
|
||||
class HomeScreenViewModelTests: XCTestCase {
|
||||
var viewModel: HomeScreenViewModelProtocol!
|
||||
var clientProxy: ClientProxyMock!
|
||||
var context: HomeScreenViewModelType.Context! { viewModel.context }
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
var roomSummaryProvider: RoomSummaryProviderMock!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
var clientProxy: ClientProxyMock!
|
||||
var roomSummaryProvider: RoomSummaryProviderMock!
|
||||
var appSettings: AppSettings!
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUp() {
|
||||
cancellables.removeAll()
|
||||
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
@ -29,26 +36,26 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
func testSelectRoom() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomId = "mock_room_id"
|
||||
let mockRoomID = "mock_room_id"
|
||||
var correctResult = false
|
||||
var selectedRoomId = ""
|
||||
var selectedRoomID = ""
|
||||
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .presentRoom(let roomId):
|
||||
case .presentRoom(let roomID):
|
||||
correctResult = true
|
||||
selectedRoomId = roomId
|
||||
selectedRoomID = roomID
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: mockRoomId))
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: mockRoomID))
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
XCTAssertEqual(mockRoomId, selectedRoomId)
|
||||
XCTAssertEqual(mockRoomID, selectedRoomID)
|
||||
}
|
||||
|
||||
func testTapUserAvatar() async throws {
|
||||
@ -75,26 +82,26 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
func testLeaveRoomAlert() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomId = "1"
|
||||
let mockRoomID = "1"
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(JoinedRoomProxyMock(.init(id: mockRoomId, name: "Some room"))) }
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(JoinedRoomProxyMock(.init(id: mockRoomID, name: "Some room"))) }
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { value in
|
||||
value.bindings.leaveRoomAlertItem != nil
|
||||
}
|
||||
|
||||
context.send(viewAction: .leaveRoom(roomIdentifier: mockRoomId))
|
||||
context.send(viewAction: .leaveRoom(roomIdentifier: mockRoomID))
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.leaveRoomAlertItem?.roomID, mockRoomId)
|
||||
XCTAssertEqual(context.leaveRoomAlertItem?.roomID, mockRoomID)
|
||||
}
|
||||
|
||||
func testLeaveRoomError() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomId = "1"
|
||||
let room = JoinedRoomProxyMock(.init(id: mockRoomId, name: "Some room"))
|
||||
let mockRoomID = "1"
|
||||
let room = JoinedRoomProxyMock(.init(id: mockRoomID, name: "Some room"))
|
||||
room.leaveRoomClosure = { .failure(.sdkError(ClientProxyMockError.generic)) }
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||
@ -103,7 +110,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
value.bindings.alertInfo != nil
|
||||
}
|
||||
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomId))
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
@ -113,26 +120,26 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
func testLeaveRoomSuccess() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomId = "1"
|
||||
let mockRoomID = "1"
|
||||
var correctResult = false
|
||||
let expectation = expectation(description: #function)
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .roomLeft(let roomIdentifier):
|
||||
correctResult = roomIdentifier == mockRoomId
|
||||
correctResult = roomIdentifier == mockRoomID
|
||||
default:
|
||||
break
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
let room = JoinedRoomProxyMock(.init(id: mockRoomId, name: "Some room"))
|
||||
let room = JoinedRoomProxyMock(.init(id: mockRoomID, name: "Some room"))
|
||||
room.leaveRoomClosure = { .success(()) }
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomId))
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
@ -141,19 +148,19 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
func testShowRoomDetails() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomId = "1"
|
||||
let mockRoomID = "1"
|
||||
var correctResult = false
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .presentRoomDetails(let roomIdentifier):
|
||||
correctResult = roomIdentifier == mockRoomId
|
||||
correctResult = roomIdentifier == mockRoomID
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomId))
|
||||
context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomID))
|
||||
await Task.yield()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
@ -256,12 +263,88 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
}
|
||||
|
||||
func testInviteUnreadBadge() async throws {
|
||||
setupViewModel(withInvites: true)
|
||||
var invites = context.viewState.rooms.invites
|
||||
XCTAssertEqual(invites.count, 2)
|
||||
|
||||
for invite in invites {
|
||||
XCTAssertTrue(invite.badges.isDotShown)
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.rooms.contains { room in
|
||||
room.roomID == invites[0].roomID && room.badges.isDotShown == false
|
||||
}
|
||||
}
|
||||
appSettings.seenInvites = Set(invites.compactMap(\.roomID))
|
||||
try await deferred.fulfill()
|
||||
invites = context.viewState.rooms.invites
|
||||
|
||||
for invite in invites {
|
||||
XCTAssertFalse(invite.badges.isDotShown)
|
||||
}
|
||||
}
|
||||
|
||||
func testAcceptInvite() async throws {
|
||||
setupViewModel(withInvites: true)
|
||||
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .presentRoom(roomIdentifier: invitedRoomIDs[0]) }
|
||||
context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
}
|
||||
|
||||
func testDeclineInvite() async throws {
|
||||
setupViewModel(withInvites: true)
|
||||
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
let rejectExpectation = expectation(description: "Expected rejectInvitation to be called.")
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
let roomProxy = InvitedRoomProxyMock(.init())
|
||||
roomProxy.rejectInvitationClosure = {
|
||||
rejectExpectation.fulfill()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
return .invited(roomProxy)
|
||||
}
|
||||
context.viewState.bindings.alertInfo?.primaryButton.action?()
|
||||
await fulfillment(of: [rejectExpectation], timeout: 1.0)
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil) {
|
||||
roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, withInvites: Bool = false) {
|
||||
var rooms: [RoomSummary] = .mockRooms
|
||||
if withInvites {
|
||||
rooms += .mockInvites
|
||||
}
|
||||
|
||||
roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(rooms)))
|
||||
|
||||
clientProxy = ClientProxyMock(.init(userID: "@mock:client.com",
|
||||
roomSummaryProvider: roomSummaryProvider))
|
||||
if withInvites {
|
||||
clientProxy.joinRoomViaReturnValue = .success(())
|
||||
clientProxy.joinRoomAliasReturnValue = .success(())
|
||||
clientProxy.roomForIdentifierClosure = { _ in .invited(InvitedRoomProxyMock(.init())) }
|
||||
}
|
||||
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
if let securityStatePublisher {
|
||||
userSession.sessionSecurityStatePublisher = securityStatePublisher
|
||||
@ -269,8 +352,20 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appSettings: appSettings,
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}
|
||||
}
|
||||
|
||||
private extension [HomeScreenRoom] {
|
||||
var invites: [HomeScreenRoom] {
|
||||
filter { room in
|
||||
if case .invite = room.type {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,20 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
var viewModel: JoinRoomScreenViewModelProtocol!
|
||||
|
||||
var clientProxy: ClientProxyMock!
|
||||
var appSettings: AppSettings!
|
||||
|
||||
var context: JoinRoomScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
viewModel = nil
|
||||
clientProxy = nil
|
||||
@ -32,7 +40,12 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testInteraction() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
setupViewModel()
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .joinable }.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.")
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined }
|
||||
context.send(viewAction: .join)
|
||||
@ -40,34 +53,45 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testAcceptInviteInteraction() async throws {
|
||||
setupViewModel()
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
setupViewModel(mode: .invited)
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .invited(isDM: false) }.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, ["1"], "The invited room's ID should be registered as a seen invite.")
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined }
|
||||
context.send(viewAction: .acceptInvite)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "The after accepting an invite the invite should be forgotten in case the user leaves.")
|
||||
}
|
||||
|
||||
func testDeclineInviteInteraction() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
setupViewModel(mode: .invited)
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.roomDetails != nil }.fulfill()
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .invited(isDM: false) }.fulfill()
|
||||
XCTAssertEqual(appSettings.seenInvites, ["1"], "The invited room's ID should be registered as a seen invite.")
|
||||
|
||||
context.send(viewAction: .declineInvite)
|
||||
|
||||
XCTAssertEqual(viewModel.context.alertInfo?.id, .declineInvite)
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss
|
||||
}
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .dismiss }
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "The after declining an invite the invite should be forgotten in case another invite is received.")
|
||||
}
|
||||
|
||||
func testKnockedState() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
setupViewModel(mode: .knocked)
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { state in
|
||||
state.mode == .knocked
|
||||
}.fulfill()
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .knocked }.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.")
|
||||
}
|
||||
|
||||
func testCancelKnock() async throws {
|
||||
@ -129,6 +153,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
clientProxy = ClientProxyMock(.init())
|
||||
|
||||
clientProxy.joinRoomViaReturnValue = throwing ? .failure(.sdkError(ClientProxyMockError.generic)) : .success(())
|
||||
clientProxy.joinRoomAliasReturnValue = clientProxy.joinRoomViaReturnValue
|
||||
|
||||
switch mode {
|
||||
case .knocked:
|
||||
@ -160,7 +185,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
viewModel = JoinRoomScreenViewModel(roomID: "1",
|
||||
via: [],
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appSettings: appSettings,
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
|
@ -80,7 +80,7 @@ class LoggingTests: XCTestCase {
|
||||
let heroName = "Pseudonym"
|
||||
let roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
|
||||
id: "myroomid",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: roomName,
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
|
@ -56,7 +56,7 @@ class RoomSummaryTests: XCTestCase {
|
||||
func makeSummary(isDirect: Bool, hasRoomAvatar: Bool) -> RoomSummary {
|
||||
RoomSummary(roomListItem: .init(noPointer: .init()),
|
||||
id: roomDetails.id,
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: roomDetails.name,
|
||||
isDirect: isDirect,
|
||||
avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil,
|
||||
|
Loading…
x
Reference in New Issue
Block a user