mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 20:38:41 +00:00
Leave Room (#699)
* created the row in the view and the alert, and added the new function to the RoomProxy * fixed an issue with the alert function * handling the navigation * fixed a bug with the detail coordinators being dismissed incorrectly when inside a stack * implementation completed * replaced UI screenshots * added a test for the fixed bug of the coordinators * trying to increase the wait time for the expectation * improved the test * improved the buttons UI * uploading artifacts for unit tests * added result bundle true * improved the tests * added a new test * pr suggestions * updating mock * PR suggestions * improved tests * fixed UI tests * pr should be ready now * removed testing code * reduced complexity * fixed test * added a an assert to the new test case * more tests and messages cases * pr comments addressed * completed
This commit is contained in:
@@ -42,6 +42,15 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: bundle exec fastlane unit_tests
|
run: bundle exec fastlane unit_tests
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: test-output
|
||||||
|
path: fastlane/test_output
|
||||||
|
retention-days: 7
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -73,6 +73,10 @@
|
|||||||
"room_details_title" = "Info";
|
"room_details_title" = "Info";
|
||||||
"room_details_about_section_title" = "About";
|
"room_details_about_section_title" = "About";
|
||||||
"room_details_copy_link" = "Copy Link";
|
"room_details_copy_link" = "Copy Link";
|
||||||
|
"room_details_leave_room_alert_subtitle" = "Are you sure that you want to leave the room?";
|
||||||
|
"room_details_leave_private_room_alert_subtitle" = "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite.";
|
||||||
|
"room_details_leave_empty_room_alert_subtitle" = "Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you.";
|
||||||
|
"room_details_room_left_toast" = "Room left";
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
"ftue_auth_carousel_welcome_title" = "Be in your Element";
|
"ftue_auth_carousel_welcome_title" = "Be in your Element";
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ extension ElementL10n {
|
|||||||
public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title")
|
public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title")
|
||||||
/// Copy Link
|
/// Copy Link
|
||||||
public static let roomDetailsCopyLink = ElementL10n.tr("Untranslated", "room_details_copy_link")
|
public static let roomDetailsCopyLink = ElementL10n.tr("Untranslated", "room_details_copy_link")
|
||||||
|
/// Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you.
|
||||||
|
public static let roomDetailsLeaveEmptyRoomAlertSubtitle = ElementL10n.tr("Untranslated", "room_details_leave_empty_room_alert_subtitle")
|
||||||
|
/// Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite.
|
||||||
|
public static let roomDetailsLeavePrivateRoomAlertSubtitle = ElementL10n.tr("Untranslated", "room_details_leave_private_room_alert_subtitle")
|
||||||
|
/// Are you sure that you want to leave the room?
|
||||||
|
public static let roomDetailsLeaveRoomAlertSubtitle = ElementL10n.tr("Untranslated", "room_details_leave_room_alert_subtitle")
|
||||||
|
/// Room left
|
||||||
|
public static let roomDetailsRoomLeftToast = ElementL10n.tr("Untranslated", "room_details_room_left_toast")
|
||||||
/// Info
|
/// Info
|
||||||
public static let roomDetailsTitle = ElementL10n.tr("Untranslated", "room_details_title")
|
public static let roomDetailsTitle = ElementL10n.tr("Untranslated", "room_details_title")
|
||||||
/// Failed loading messages
|
/// Failed loading messages
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
// swiftlint:disable all
|
// swiftlint:disable all
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
class BugReportServiceMock: BugReportServiceProtocol {
|
class BugReportServiceMock: BugReportServiceProtocol {
|
||||||
var crashedLastRun: Bool {
|
var crashedLastRun: Bool {
|
||||||
@@ -49,6 +50,322 @@ class BugReportServiceMock: BugReportServiceProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class RoomProxyMock: RoomProxyProtocol {
|
||||||
|
var id: String {
|
||||||
|
get { return underlyingId }
|
||||||
|
set(value) { underlyingId = value }
|
||||||
|
}
|
||||||
|
var underlyingId: String!
|
||||||
|
var isDirect: Bool {
|
||||||
|
get { return underlyingIsDirect }
|
||||||
|
set(value) { underlyingIsDirect = value }
|
||||||
|
}
|
||||||
|
var underlyingIsDirect: Bool!
|
||||||
|
var isPublic: Bool {
|
||||||
|
get { return underlyingIsPublic }
|
||||||
|
set(value) { underlyingIsPublic = value }
|
||||||
|
}
|
||||||
|
var underlyingIsPublic: Bool!
|
||||||
|
var isSpace: Bool {
|
||||||
|
get { return underlyingIsSpace }
|
||||||
|
set(value) { underlyingIsSpace = value }
|
||||||
|
}
|
||||||
|
var underlyingIsSpace: Bool!
|
||||||
|
var isEncrypted: Bool {
|
||||||
|
get { return underlyingIsEncrypted }
|
||||||
|
set(value) { underlyingIsEncrypted = value }
|
||||||
|
}
|
||||||
|
var underlyingIsEncrypted: Bool!
|
||||||
|
var isTombstoned: Bool {
|
||||||
|
get { return underlyingIsTombstoned }
|
||||||
|
set(value) { underlyingIsTombstoned = value }
|
||||||
|
}
|
||||||
|
var underlyingIsTombstoned: Bool!
|
||||||
|
var canonicalAlias: String?
|
||||||
|
var alternativeAliases: [String] = []
|
||||||
|
var hasUnreadNotifications: Bool {
|
||||||
|
get { return underlyingHasUnreadNotifications }
|
||||||
|
set(value) { underlyingHasUnreadNotifications = value }
|
||||||
|
}
|
||||||
|
var underlyingHasUnreadNotifications: Bool!
|
||||||
|
var name: String?
|
||||||
|
var displayName: String?
|
||||||
|
var topic: String?
|
||||||
|
var avatarURL: URL?
|
||||||
|
|
||||||
|
//MARK: - loadAvatarURLForUserId
|
||||||
|
|
||||||
|
var loadAvatarURLForUserIdCallsCount = 0
|
||||||
|
var loadAvatarURLForUserIdCalled: Bool {
|
||||||
|
return loadAvatarURLForUserIdCallsCount > 0
|
||||||
|
}
|
||||||
|
var loadAvatarURLForUserIdReceivedUserId: String?
|
||||||
|
var loadAvatarURLForUserIdReceivedInvocations: [String] = []
|
||||||
|
var loadAvatarURLForUserIdReturnValue: Result<URL?, RoomProxyError>!
|
||||||
|
var loadAvatarURLForUserIdClosure: ((String) async -> Result<URL?, RoomProxyError>)?
|
||||||
|
|
||||||
|
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> {
|
||||||
|
loadAvatarURLForUserIdCallsCount += 1
|
||||||
|
loadAvatarURLForUserIdReceivedUserId = userId
|
||||||
|
loadAvatarURLForUserIdReceivedInvocations.append(userId)
|
||||||
|
if let loadAvatarURLForUserIdClosure = loadAvatarURLForUserIdClosure {
|
||||||
|
return await loadAvatarURLForUserIdClosure(userId)
|
||||||
|
} else {
|
||||||
|
return loadAvatarURLForUserIdReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - loadDisplayNameForUserId
|
||||||
|
|
||||||
|
var loadDisplayNameForUserIdCallsCount = 0
|
||||||
|
var loadDisplayNameForUserIdCalled: Bool {
|
||||||
|
return loadDisplayNameForUserIdCallsCount > 0
|
||||||
|
}
|
||||||
|
var loadDisplayNameForUserIdReceivedUserId: String?
|
||||||
|
var loadDisplayNameForUserIdReceivedInvocations: [String] = []
|
||||||
|
var loadDisplayNameForUserIdReturnValue: Result<String?, RoomProxyError>!
|
||||||
|
var loadDisplayNameForUserIdClosure: ((String) async -> Result<String?, RoomProxyError>)?
|
||||||
|
|
||||||
|
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||||
|
loadDisplayNameForUserIdCallsCount += 1
|
||||||
|
loadDisplayNameForUserIdReceivedUserId = userId
|
||||||
|
loadDisplayNameForUserIdReceivedInvocations.append(userId)
|
||||||
|
if let loadDisplayNameForUserIdClosure = loadDisplayNameForUserIdClosure {
|
||||||
|
return await loadDisplayNameForUserIdClosure(userId)
|
||||||
|
} else {
|
||||||
|
return loadDisplayNameForUserIdReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - addTimelineListener
|
||||||
|
|
||||||
|
var addTimelineListenerListenerCallsCount = 0
|
||||||
|
var addTimelineListenerListenerCalled: Bool {
|
||||||
|
return addTimelineListenerListenerCallsCount > 0
|
||||||
|
}
|
||||||
|
var addTimelineListenerListenerReceivedListener: TimelineListener?
|
||||||
|
var addTimelineListenerListenerReceivedInvocations: [TimelineListener] = []
|
||||||
|
var addTimelineListenerListenerReturnValue: Result<[TimelineItem], RoomProxyError>!
|
||||||
|
var addTimelineListenerListenerClosure: ((TimelineListener) -> Result<[TimelineItem], RoomProxyError>)?
|
||||||
|
|
||||||
|
func addTimelineListener(listener: TimelineListener) -> Result<[TimelineItem], RoomProxyError> {
|
||||||
|
addTimelineListenerListenerCallsCount += 1
|
||||||
|
addTimelineListenerListenerReceivedListener = listener
|
||||||
|
addTimelineListenerListenerReceivedInvocations.append(listener)
|
||||||
|
if let addTimelineListenerListenerClosure = addTimelineListenerListenerClosure {
|
||||||
|
return addTimelineListenerListenerClosure(listener)
|
||||||
|
} else {
|
||||||
|
return addTimelineListenerListenerReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - removeTimelineListener
|
||||||
|
|
||||||
|
var removeTimelineListenerCallsCount = 0
|
||||||
|
var removeTimelineListenerCalled: Bool {
|
||||||
|
return removeTimelineListenerCallsCount > 0
|
||||||
|
}
|
||||||
|
var removeTimelineListenerClosure: (() -> Void)?
|
||||||
|
|
||||||
|
func removeTimelineListener() {
|
||||||
|
removeTimelineListenerCallsCount += 1
|
||||||
|
removeTimelineListenerClosure?()
|
||||||
|
}
|
||||||
|
//MARK: - paginateBackwards
|
||||||
|
|
||||||
|
var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount = 0
|
||||||
|
var paginateBackwardsRequestSizeUntilNumberOfItemsCalled: Bool {
|
||||||
|
return paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount > 0
|
||||||
|
}
|
||||||
|
var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments: (requestSize: UInt, untilNumberOfItems: UInt)?
|
||||||
|
var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations: [(requestSize: UInt, untilNumberOfItems: UInt)] = []
|
||||||
|
var paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var paginateBackwardsRequestSizeUntilNumberOfItemsClosure: ((UInt, UInt) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> {
|
||||||
|
paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount += 1
|
||||||
|
paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments = (requestSize: requestSize, untilNumberOfItems: untilNumberOfItems)
|
||||||
|
paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations.append((requestSize: requestSize, untilNumberOfItems: untilNumberOfItems))
|
||||||
|
if let paginateBackwardsRequestSizeUntilNumberOfItemsClosure = paginateBackwardsRequestSizeUntilNumberOfItemsClosure {
|
||||||
|
return await paginateBackwardsRequestSizeUntilNumberOfItemsClosure(requestSize, untilNumberOfItems)
|
||||||
|
} else {
|
||||||
|
return paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - sendReadReceipt
|
||||||
|
|
||||||
|
var sendReadReceiptForCallsCount = 0
|
||||||
|
var sendReadReceiptForCalled: Bool {
|
||||||
|
return sendReadReceiptForCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendReadReceiptForReceivedEventID: String?
|
||||||
|
var sendReadReceiptForReceivedInvocations: [String] = []
|
||||||
|
var sendReadReceiptForReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendReadReceiptForClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendReadReceiptForCallsCount += 1
|
||||||
|
sendReadReceiptForReceivedEventID = eventID
|
||||||
|
sendReadReceiptForReceivedInvocations.append(eventID)
|
||||||
|
if let sendReadReceiptForClosure = sendReadReceiptForClosure {
|
||||||
|
return await sendReadReceiptForClosure(eventID)
|
||||||
|
} else {
|
||||||
|
return sendReadReceiptForReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - sendMessage
|
||||||
|
|
||||||
|
var sendMessageInReplyToCallsCount = 0
|
||||||
|
var sendMessageInReplyToCalled: Bool {
|
||||||
|
return sendMessageInReplyToCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendMessageInReplyToReceivedArguments: (message: String, eventID: String?)?
|
||||||
|
var sendMessageInReplyToReceivedInvocations: [(message: String, eventID: String?)] = []
|
||||||
|
var sendMessageInReplyToReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendMessageInReplyToClosure: ((String, String?) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendMessage(_ message: String, inReplyTo eventID: String?) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendMessageInReplyToCallsCount += 1
|
||||||
|
sendMessageInReplyToReceivedArguments = (message: message, eventID: eventID)
|
||||||
|
sendMessageInReplyToReceivedInvocations.append((message: message, eventID: eventID))
|
||||||
|
if let sendMessageInReplyToClosure = sendMessageInReplyToClosure {
|
||||||
|
return await sendMessageInReplyToClosure(message, eventID)
|
||||||
|
} else {
|
||||||
|
return sendMessageInReplyToReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - sendReaction
|
||||||
|
|
||||||
|
var sendReactionToCallsCount = 0
|
||||||
|
var sendReactionToCalled: Bool {
|
||||||
|
return sendReactionToCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendReactionToReceivedArguments: (reaction: String, eventID: String)?
|
||||||
|
var sendReactionToReceivedInvocations: [(reaction: String, eventID: String)] = []
|
||||||
|
var sendReactionToReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendReactionToClosure: ((String, String) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendReactionToCallsCount += 1
|
||||||
|
sendReactionToReceivedArguments = (reaction: reaction, eventID: eventID)
|
||||||
|
sendReactionToReceivedInvocations.append((reaction: reaction, eventID: eventID))
|
||||||
|
if let sendReactionToClosure = sendReactionToClosure {
|
||||||
|
return await sendReactionToClosure(reaction, eventID)
|
||||||
|
} else {
|
||||||
|
return sendReactionToReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - editMessage
|
||||||
|
|
||||||
|
var editMessageOriginalCallsCount = 0
|
||||||
|
var editMessageOriginalCalled: Bool {
|
||||||
|
return editMessageOriginalCallsCount > 0
|
||||||
|
}
|
||||||
|
var editMessageOriginalReceivedArguments: (newMessage: String, eventID: String)?
|
||||||
|
var editMessageOriginalReceivedInvocations: [(newMessage: String, eventID: String)] = []
|
||||||
|
var editMessageOriginalReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var editMessageOriginalClosure: ((String, String) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
editMessageOriginalCallsCount += 1
|
||||||
|
editMessageOriginalReceivedArguments = (newMessage: newMessage, eventID: eventID)
|
||||||
|
editMessageOriginalReceivedInvocations.append((newMessage: newMessage, eventID: eventID))
|
||||||
|
if let editMessageOriginalClosure = editMessageOriginalClosure {
|
||||||
|
return await editMessageOriginalClosure(newMessage, eventID)
|
||||||
|
} else {
|
||||||
|
return editMessageOriginalReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - redact
|
||||||
|
|
||||||
|
var redactCallsCount = 0
|
||||||
|
var redactCalled: Bool {
|
||||||
|
return redactCallsCount > 0
|
||||||
|
}
|
||||||
|
var redactReceivedEventID: String?
|
||||||
|
var redactReceivedInvocations: [String] = []
|
||||||
|
var redactReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var redactClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
redactCallsCount += 1
|
||||||
|
redactReceivedEventID = eventID
|
||||||
|
redactReceivedInvocations.append(eventID)
|
||||||
|
if let redactClosure = redactClosure {
|
||||||
|
return await redactClosure(eventID)
|
||||||
|
} else {
|
||||||
|
return redactReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - reportContent
|
||||||
|
|
||||||
|
var reportContentReasonCallsCount = 0
|
||||||
|
var reportContentReasonCalled: Bool {
|
||||||
|
return reportContentReasonCallsCount > 0
|
||||||
|
}
|
||||||
|
var reportContentReasonReceivedArguments: (eventID: String, reason: String?)?
|
||||||
|
var reportContentReasonReceivedInvocations: [(eventID: String, reason: String?)] = []
|
||||||
|
var reportContentReasonReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var reportContentReasonClosure: ((String, String?) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
||||||
|
reportContentReasonCallsCount += 1
|
||||||
|
reportContentReasonReceivedArguments = (eventID: eventID, reason: reason)
|
||||||
|
reportContentReasonReceivedInvocations.append((eventID: eventID, reason: reason))
|
||||||
|
if let reportContentReasonClosure = reportContentReasonClosure {
|
||||||
|
return await reportContentReasonClosure(eventID, reason)
|
||||||
|
} else {
|
||||||
|
return reportContentReasonReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - members
|
||||||
|
|
||||||
|
var membersCallsCount = 0
|
||||||
|
var membersCalled: Bool {
|
||||||
|
return membersCallsCount > 0
|
||||||
|
}
|
||||||
|
var membersReturnValue: Result<[RoomMemberProxy], RoomProxyError>!
|
||||||
|
var membersClosure: (() async -> Result<[RoomMemberProxy], RoomProxyError>)?
|
||||||
|
|
||||||
|
func members() async -> Result<[RoomMemberProxy], RoomProxyError> {
|
||||||
|
membersCallsCount += 1
|
||||||
|
if let membersClosure = membersClosure {
|
||||||
|
return await membersClosure()
|
||||||
|
} else {
|
||||||
|
return membersReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - retryDecryption
|
||||||
|
|
||||||
|
var retryDecryptionForCallsCount = 0
|
||||||
|
var retryDecryptionForCalled: Bool {
|
||||||
|
return retryDecryptionForCallsCount > 0
|
||||||
|
}
|
||||||
|
var retryDecryptionForReceivedSessionID: String?
|
||||||
|
var retryDecryptionForReceivedInvocations: [String] = []
|
||||||
|
var retryDecryptionForClosure: ((String) async -> Void)?
|
||||||
|
|
||||||
|
func retryDecryption(for sessionID: String) async {
|
||||||
|
retryDecryptionForCallsCount += 1
|
||||||
|
retryDecryptionForReceivedSessionID = sessionID
|
||||||
|
retryDecryptionForReceivedInvocations.append(sessionID)
|
||||||
|
await retryDecryptionForClosure?(sessionID)
|
||||||
|
}
|
||||||
|
//MARK: - leaveRoom
|
||||||
|
|
||||||
|
var leaveRoomCallsCount = 0
|
||||||
|
var leaveRoomCalled: Bool {
|
||||||
|
return leaveRoomCallsCount > 0
|
||||||
|
}
|
||||||
|
var leaveRoomReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var leaveRoomClosure: (() async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func leaveRoom() async -> Result<Void, RoomProxyError> {
|
||||||
|
leaveRoomCallsCount += 1
|
||||||
|
if let leaveRoomClosure = leaveRoomClosure {
|
||||||
|
return await leaveRoomClosure()
|
||||||
|
} else {
|
||||||
|
return leaveRoomReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
||||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
||||||
get { return underlyingCallbacks }
|
get { return underlyingCallbacks }
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct RoomProxyMockConfiguration {
|
||||||
|
var id = UUID().uuidString
|
||||||
|
let name: String? = nil
|
||||||
|
let displayName: String?
|
||||||
|
var topic: String?
|
||||||
|
var avatarURL: URL?
|
||||||
|
var isDirect = Bool.random()
|
||||||
|
var isSpace = Bool.random()
|
||||||
|
var isPublic = Bool.random()
|
||||||
|
var isEncrypted = Bool.random()
|
||||||
|
var isTombstoned = Bool.random()
|
||||||
|
var canonicalAlias: String?
|
||||||
|
var alternativeAliases: [String] = []
|
||||||
|
var hasUnreadNotifications = Bool.random()
|
||||||
|
var members: [RoomMemberProxy]?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RoomProxyMock {
|
||||||
|
convenience init(with configuration: RoomProxyMockConfiguration) {
|
||||||
|
self.init()
|
||||||
|
|
||||||
|
id = configuration.id
|
||||||
|
name = configuration.name
|
||||||
|
displayName = configuration.displayName
|
||||||
|
topic = configuration.topic
|
||||||
|
avatarURL = configuration.avatarURL
|
||||||
|
isDirect = configuration.isDirect
|
||||||
|
isSpace = configuration.isSpace
|
||||||
|
isPublic = configuration.isPublic
|
||||||
|
isEncrypted = configuration.isEncrypted
|
||||||
|
isTombstoned = configuration.isTombstoned
|
||||||
|
canonicalAlias = configuration.canonicalAlias
|
||||||
|
alternativeAliases = configuration.alternativeAliases
|
||||||
|
hasUnreadNotifications = configuration.hasUnreadNotifications
|
||||||
|
|
||||||
|
membersClosure = {
|
||||||
|
if let members = configuration.members {
|
||||||
|
return .success(members)
|
||||||
|
}
|
||||||
|
return .failure(.failedRetrievingMembers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ protocol AlertItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func alert<I, V>(item: Binding<I?>, actions: (I) -> V, message: (I) -> V) -> some View where I: AlertItem, V: View {
|
func alert<Item, Actions, Message>(item: Binding<Item?>, actions: (Item) -> Actions, message: (Item) -> Message) -> some View where Item: AlertItem, Actions: View, Message: View {
|
||||||
let binding = Binding<Bool>(get: {
|
let binding = Binding<Bool>(get: {
|
||||||
item.wrappedValue != nil
|
item.wrappedValue != nil
|
||||||
}, set: { newValue in
|
}, set: { newValue in
|
||||||
@@ -32,7 +32,7 @@ extension View {
|
|||||||
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message)
|
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func alert<I, V>(item: Binding<I?>, actions: (I) -> V) -> some View where I: AlertItem, V: View {
|
func alert<Item, Actions>(item: Binding<Item?>, actions: (Item) -> Actions) -> some View where Item: AlertItem, Actions: View {
|
||||||
let binding = Binding<Bool>(get: {
|
let binding = Binding<Bool>(get: {
|
||||||
item.wrappedValue != nil
|
item.wrappedValue != nil
|
||||||
}, set: { newValue in
|
}, set: { newValue in
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
|||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
Button(action: configuration.trigger) {
|
Button(action: configuration.trigger) {
|
||||||
configuration.label
|
configuration.label
|
||||||
|
.labelStyle(FormRowLabelStyle(role: configuration.role))
|
||||||
.frame(maxHeight: .infinity) // Make sure the label fills the cell vertically.
|
.frame(maxHeight: .infinity) // Make sure the label fills the cell vertically.
|
||||||
}
|
}
|
||||||
.buttonStyle(Style(accessory: accessory))
|
.buttonStyle(Style(accessory: accessory))
|
||||||
@@ -54,7 +55,7 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
|||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
configuration.label
|
configuration.label
|
||||||
.labelStyle(FormRowLabelStyle())
|
.labelStyle(FormRowLabelStyle(role: configuration.role))
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(.element.primaryContent)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
@@ -105,6 +106,11 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||||
|
|
||||||
|
Button(role: .destructive) { } label: {
|
||||||
|
Label("Show destruction", systemImage: "rectangle.portrait")
|
||||||
|
}
|
||||||
|
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||||
|
|
||||||
ShareLink(item: "test")
|
ShareLink(item: "test")
|
||||||
.buttonStyle(FormButtonStyle())
|
.buttonStyle(FormButtonStyle())
|
||||||
}
|
}
|
||||||
@@ -117,6 +123,14 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
|||||||
.buttonStyle(FormButtonStyle())
|
.buttonStyle(FormButtonStyle())
|
||||||
}
|
}
|
||||||
.formSectionStyle()
|
.formSectionStyle()
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) { } label: {
|
||||||
|
Label("Destroy", systemImage: "x.circle")
|
||||||
|
}
|
||||||
|
.buttonStyle(FormButtonStyle())
|
||||||
|
}
|
||||||
|
.formSectionStyle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,19 +19,44 @@ import SwiftUI
|
|||||||
struct FormRowLabelStyle: LabelStyle {
|
struct FormRowLabelStyle: LabelStyle {
|
||||||
@ScaledMetric private var menuIconSize = 30.0
|
@ScaledMetric private var menuIconSize = 30.0
|
||||||
|
|
||||||
var alignment: VerticalAlignment = .firstTextBaseline
|
var alignment = VerticalAlignment.firstTextBaseline
|
||||||
|
var role: ButtonRole?
|
||||||
|
|
||||||
|
private var titleColor: Color {
|
||||||
|
if role == .destructive {
|
||||||
|
return .element.alert
|
||||||
|
} else {
|
||||||
|
return .element.primaryContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconBackgroundColor: Color {
|
||||||
|
if role == .destructive {
|
||||||
|
return .element.alert.opacity(0.1)
|
||||||
|
} else {
|
||||||
|
return .element.formBackground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconForegroundColor: Color {
|
||||||
|
if role == .destructive {
|
||||||
|
return .element.alert
|
||||||
|
} else {
|
||||||
|
return .element.secondaryContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
HStack(alignment: alignment, spacing: 16) {
|
HStack(alignment: alignment, spacing: 16) {
|
||||||
configuration.icon
|
configuration.icon
|
||||||
.foregroundColor(.element.secondaryContent)
|
.foregroundColor(iconForegroundColor)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
.frame(width: menuIconSize, height: menuIconSize)
|
.frame(width: menuIconSize, height: menuIconSize)
|
||||||
.background(Color.element.formBackground)
|
.background(iconBackgroundColor)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
configuration.title
|
configuration.title
|
||||||
.font(.element.body)
|
.font(.element.body)
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(titleColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +75,9 @@ struct FormRowLabelStyle_Previews: PreviewProvider {
|
|||||||
|
|
||||||
Label("Help", systemImage: "questionmark")
|
Label("Help", systemImage: "questionmark")
|
||||||
.labelStyle(FormRowLabelStyle())
|
.labelStyle(FormRowLabelStyle())
|
||||||
|
|
||||||
|
Label("Destroy", systemImage: "x.circle")
|
||||||
|
.labelStyle(FormRowLabelStyle(role: .destructive))
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct UserIndicatorToastView: View {
|
|||||||
.padding(.horizontal, 12.0)
|
.padding(.horizontal, 12.0)
|
||||||
.padding(.vertical, 10.0)
|
.padding(.vertical, 10.0)
|
||||||
.frame(minWidth: 150.0)
|
.frame(minWidth: 150.0)
|
||||||
.background(Color.element.quaternaryContent)
|
.background(Color.element.system)
|
||||||
.clipShape(RoundedCornerShape(radius: 24.0, corners: .allCorners))
|
.clipShape(RoundedCornerShape(radius: 24.0, corners: .allCorners))
|
||||||
.shadow(color: .black.opacity(0.1), radius: 10.0, y: 4.0)
|
.shadow(color: .black.opacity(0.1), radius: 10.0, y: 4.0)
|
||||||
.transition(toastTransition)
|
.transition(toastTransition)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ struct ReportContentScreen: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct ReportContent_Previews: PreviewProvider {
|
struct ReportContent_Previews: PreviewProvider {
|
||||||
static let viewModel = ReportContentViewModel(itemID: "", roomProxy: MockRoomProxy(displayName: nil))
|
static let viewModel = ReportContentViewModel(itemID: "", roomProxy: RoomProxyMock(with: .init(displayName: nil)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ReportContentScreen(context: viewModel.context)
|
ReportContentScreen(context: viewModel.context)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct RoomDetailsCoordinatorParameters {
|
|||||||
|
|
||||||
enum RoomDetailsCoordinatorAction {
|
enum RoomDetailsCoordinatorAction {
|
||||||
case cancel
|
case cancel
|
||||||
|
case leftRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
final class RoomDetailsCoordinator: CoordinatorProtocol {
|
final class RoomDetailsCoordinator: CoordinatorProtocol {
|
||||||
@@ -51,6 +52,8 @@ final class RoomDetailsCoordinator: CoordinatorProtocol {
|
|||||||
self.presentRoomMemberDetails(members)
|
self.presentRoomMemberDetails(members)
|
||||||
case .cancel:
|
case .cancel:
|
||||||
self.callback?(.cancel)
|
self.callback?(.cancel)
|
||||||
|
case .leftRoom:
|
||||||
|
self.callback?(.leftRoom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import UIKit
|
|||||||
|
|
||||||
enum RoomDetailsViewModelAction {
|
enum RoomDetailsViewModelAction {
|
||||||
case requestMemberDetailsPresentation([RoomMemberProxy])
|
case requestMemberDetailsPresentation([RoomMemberProxy])
|
||||||
|
case leftRoom
|
||||||
case cancel
|
case cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,15 +50,34 @@ struct RoomDetailsViewState: BindableState {
|
|||||||
struct RoomDetailsViewStateBindings {
|
struct RoomDetailsViewStateBindings {
|
||||||
/// Information describing the currently displayed alert.
|
/// Information describing the currently displayed alert.
|
||||||
var alertInfo: AlertInfo<RoomDetailsErrorType>?
|
var alertInfo: AlertInfo<RoomDetailsErrorType>?
|
||||||
|
var leaveRoomAlertItem: LeaveRoomAlertItem?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomDetailsErrorType: Hashable {
|
struct LeaveRoomAlertItem: AlertItem {
|
||||||
/// A specific error message shown in an alert.
|
enum RoomState {
|
||||||
case alert(String)
|
case empty
|
||||||
|
case `public`
|
||||||
|
case `private`
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: RoomState
|
||||||
|
let title = ElementL10n.roomProfileSectionMoreLeave
|
||||||
|
let confirmationTitle = ElementL10n.actionLeave
|
||||||
|
let cancelTitle = ElementL10n.actionCancel
|
||||||
|
|
||||||
|
var subtitle: String {
|
||||||
|
switch state {
|
||||||
|
case .empty: return ElementL10n.roomDetailsLeaveEmptyRoomAlertSubtitle
|
||||||
|
case .private: return ElementL10n.roomDetailsLeavePrivateRoomAlertSubtitle
|
||||||
|
case .public: return ElementL10n.roomDetailsLeaveRoomAlertSubtitle
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomDetailsViewAction {
|
enum RoomDetailsViewAction {
|
||||||
case processTapPeople
|
case processTapPeople
|
||||||
|
case processTapLeave
|
||||||
|
case confirmLeave
|
||||||
case copyRoomLink
|
case copyRoomLink
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,3 +92,10 @@ struct RoomDetailsMember: Identifiable, Equatable {
|
|||||||
avatarURL = proxy.avatarURL
|
avatarURL = proxy.avatarURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RoomDetailsErrorType: Hashable {
|
||||||
|
/// A specific error message shown in an alert.
|
||||||
|
case alert(String)
|
||||||
|
/// Leaving room has failed..
|
||||||
|
case unknown
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import SwiftUI
|
|||||||
typealias RoomDetailsViewModelType = StateStoreViewModel<RoomDetailsViewState, RoomDetailsViewAction>
|
typealias RoomDetailsViewModelType = StateStoreViewModel<RoomDetailsViewState, RoomDetailsViewAction>
|
||||||
|
|
||||||
class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtocol {
|
class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtocol {
|
||||||
|
private let roomProxy: RoomProxyProtocol
|
||||||
private var members: [RoomMemberProxy] = [] {
|
private var members: [RoomMemberProxy] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
state.members = members.map { RoomDetailsMember(withProxy: $0) }
|
state.members = members.map { RoomDetailsMember(withProxy: $0) }
|
||||||
@@ -29,6 +30,7 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
|||||||
|
|
||||||
init(roomProxy: RoomProxyProtocol,
|
init(roomProxy: RoomProxyProtocol,
|
||||||
mediaProvider: MediaProviderProtocol) {
|
mediaProvider: MediaProviderProtocol) {
|
||||||
|
self.roomProxy = roomProxy
|
||||||
super.init(initialViewState: .init(roomId: roomProxy.id,
|
super.init(initialViewState: .init(roomId: roomProxy.id,
|
||||||
canonicalAlias: roomProxy.canonicalAlias,
|
canonicalAlias: roomProxy.canonicalAlias,
|
||||||
isEncrypted: roomProxy.isEncrypted,
|
isEncrypted: roomProxy.isEncrypted,
|
||||||
@@ -60,11 +62,21 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
|||||||
callback?(.requestMemberDetailsPresentation(members))
|
callback?(.requestMemberDetailsPresentation(members))
|
||||||
case .copyRoomLink:
|
case .copyRoomLink:
|
||||||
copyRoomLink()
|
copyRoomLink()
|
||||||
|
case .processTapLeave:
|
||||||
|
guard members.count > 1 else {
|
||||||
|
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: .empty)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: roomProxy.isPublic ? .public : .private)
|
||||||
|
case .confirmLeave:
|
||||||
|
await leaveRoom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private static let leaveRoomLoadingID = "LeaveRoomLoading"
|
||||||
|
|
||||||
private func copyRoomLink() {
|
private func copyRoomLink() {
|
||||||
if let roomLink = state.permalink {
|
if let roomLink = state.permalink {
|
||||||
UIPasteboard.general.url = roomLink
|
UIPasteboard.general.url = roomLink
|
||||||
@@ -73,4 +85,16 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
|||||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: ElementL10n.unknownError))
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: ElementL10n.unknownError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func leaveRoom() async {
|
||||||
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: ElementL10n.loading, persistent: true))
|
||||||
|
let result = await roomProxy.leaveRoom()
|
||||||
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
|
||||||
|
switch result {
|
||||||
|
case .failure:
|
||||||
|
state.bindings.alertInfo = AlertInfo(id: .unknown)
|
||||||
|
case .success:
|
||||||
|
callback?(.leftRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,15 @@ struct RoomDetailsScreen: View {
|
|||||||
if context.viewState.isEncrypted {
|
if context.viewState.isEncrypted {
|
||||||
securitySection
|
securitySection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaveRoomSection
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.element.formBackground.ignoresSafeArea())
|
.background(Color.element.formBackground.ignoresSafeArea())
|
||||||
.alert(item: $context.alertInfo) { $0.alert }
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
|
.alert(item: $context.leaveRoomAlertItem,
|
||||||
|
actions: leaveRoomAlertActions,
|
||||||
|
message: leaveRoomAlertMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@@ -147,6 +152,30 @@ struct RoomDetailsScreen: View {
|
|||||||
}
|
}
|
||||||
.formSectionStyle()
|
.formSectionStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var leaveRoomSection: some View {
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
context.send(viewAction: .processTapLeave)
|
||||||
|
} label: {
|
||||||
|
Label(ElementL10n.roomProfileSectionMoreLeave, systemImage: "door.right.hand.open")
|
||||||
|
}
|
||||||
|
.buttonStyle(FormButtonStyle(accessory: nil))
|
||||||
|
}
|
||||||
|
.formSectionStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func leaveRoomAlertActions(_ item: LeaveRoomAlertItem) -> some View {
|
||||||
|
Button(item.cancelTitle, role: .cancel) { }
|
||||||
|
Button(item.confirmationTitle, role: .destructive) {
|
||||||
|
context.send(viewAction: .confirmLeave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func leaveRoomAlertMessage(_ item: LeaveRoomAlertItem) -> some View {
|
||||||
|
Text(item.subtitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
@@ -158,12 +187,12 @@ struct RoomDetails_Previews: PreviewProvider {
|
|||||||
.mockBob,
|
.mockBob,
|
||||||
.mockCharlie
|
.mockCharlie
|
||||||
]
|
]
|
||||||
let roomProxy = MockRoomProxy(displayName: "Room A",
|
let roomProxy = RoomProxyMock(with: .init(displayName: "Room A",
|
||||||
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
canonicalAlias: "#alias:domain.com",
|
canonicalAlias: "#alias:domain.com",
|
||||||
members: members)
|
members: members))
|
||||||
|
|
||||||
return RoomDetailsViewModel(roomProxy: roomProxy,
|
return RoomDetailsViewModel(roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ struct RoomScreenCoordinatorParameters {
|
|||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RoomScreenCoordinatorAction {
|
||||||
|
case leftRoom
|
||||||
|
}
|
||||||
|
|
||||||
final class RoomScreenCoordinator: CoordinatorProtocol {
|
final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||||
private var parameters: RoomScreenCoordinatorParameters
|
private var parameters: RoomScreenCoordinatorParameters
|
||||||
|
|
||||||
@@ -32,6 +36,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
parameters.navigationStackCoordinator
|
parameters.navigationStackCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var callback: ((RoomScreenCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
init(parameters: RoomScreenCoordinatorParameters) {
|
init(parameters: RoomScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
@@ -111,8 +117,13 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
roomProxy: parameters.roomProxy,
|
roomProxy: parameters.roomProxy,
|
||||||
mediaProvider: parameters.mediaProvider)
|
mediaProvider: parameters.mediaProvider)
|
||||||
let coordinator = RoomDetailsCoordinator(parameters: params)
|
let coordinator = RoomDetailsCoordinator(parameters: params)
|
||||||
coordinator.callback = { [weak self] _ in
|
coordinator.callback = { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .cancel:
|
||||||
self?.navigationStackCoordinator.pop()
|
self?.navigationStackCoordinator.pop()
|
||||||
|
case .leftRoom:
|
||||||
|
self?.callback?(.leftRoom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
switch room {
|
switch room {
|
||||||
case .empty:
|
case .empty:
|
||||||
return MockRoomProxy(displayName: "Empty room")
|
return await RoomProxyMock(with: .init(displayName: "Empty room"))
|
||||||
case .filled(let details), .invalidated(let details):
|
case .filled(let details), .invalidated(let details):
|
||||||
return MockRoomProxy(displayName: details.name)
|
return await RoomProxyMock(with: .init(displayName: details.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2022 New Vector Ltd
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
|
||||||
import MatrixRustSDK
|
|
||||||
|
|
||||||
struct MockRoomProxy: RoomProxyProtocol {
|
|
||||||
var id = UUID().uuidString
|
|
||||||
let name: String? = nil
|
|
||||||
let displayName: String?
|
|
||||||
var topic: String?
|
|
||||||
var avatarURL: URL?
|
|
||||||
var isDirect = Bool.random()
|
|
||||||
var isSpace = Bool.random()
|
|
||||||
var isPublic = Bool.random()
|
|
||||||
var isEncrypted = Bool.random()
|
|
||||||
var isTombstoned = Bool.random()
|
|
||||||
var canonicalAlias: String?
|
|
||||||
var alternativeAliases: [String] = []
|
|
||||||
var hasUnreadNotifications = Bool.random()
|
|
||||||
var members: [RoomMemberProxy]?
|
|
||||||
|
|
||||||
let timelineProvider: RoomTimelineProviderProtocol = MockRoomTimelineProvider()
|
|
||||||
|
|
||||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
|
||||||
.failure(.failedRetrievingMemberDisplayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> {
|
|
||||||
.failure(.failedRetrievingMemberAvatarURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTimelineListener(listener: TimelineListener) -> Result<[TimelineItem], RoomProxyError> {
|
|
||||||
.failure(.failedAddingTimelineListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeTimelineListener() { }
|
|
||||||
|
|
||||||
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedPaginatingBackwards)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedSendingReadReceipt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendMessage(_ message: String, inReplyTo eventID: String? = nil) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedSendingMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedSendingMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedSendingMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedRedactingEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
|
||||||
.failure(.failedReportingContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func members() async -> Result<[RoomMemberProxy], RoomProxyError> {
|
|
||||||
if let members {
|
|
||||||
return .success(members)
|
|
||||||
}
|
|
||||||
return .failure(.failedRetrievingMembers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func retryDecryption(for sessionID: String) async { }
|
|
||||||
}
|
|
||||||
@@ -288,6 +288,23 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func leaveRoom() async -> Result<Void, RoomProxyError> {
|
||||||
|
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||||
|
defer {
|
||||||
|
sendMessageBackgroundTask?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.dispatch(on: .global()) {
|
||||||
|
do {
|
||||||
|
try self.room.leave()
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to leave the room: \(error)")
|
||||||
|
return .failure(.failedLeavingRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
/// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
/// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ enum RoomProxyError: Error {
|
|||||||
case failedReportingContent
|
case failedReportingContent
|
||||||
case failedAddingTimelineListener
|
case failedAddingTimelineListener
|
||||||
case failedRetrievingMembers
|
case failedRetrievingMembers
|
||||||
|
case failedLeavingRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
// sourcery: AutoMockable
|
||||||
protocol RoomProxyProtocol {
|
protocol RoomProxyProtocol {
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
var isDirect: Bool { get }
|
var isDirect: Bool { get }
|
||||||
@@ -52,8 +54,6 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
var avatarURL: URL? { get }
|
var avatarURL: URL? { get }
|
||||||
|
|
||||||
var permalink: URL? { get }
|
|
||||||
|
|
||||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError>
|
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError>
|
||||||
|
|
||||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
||||||
@@ -79,6 +79,8 @@ protocol RoomProxyProtocol {
|
|||||||
func members() async -> Result<[RoomMemberProxy], RoomProxyError>
|
func members() async -> Result<[RoomMemberProxy], RoomProxyError>
|
||||||
|
|
||||||
func retryDecryption(for sessionID: String) async
|
func retryDecryption(for sessionID: String) async
|
||||||
|
|
||||||
|
func leaveRoom() async -> Result<Void, RoomProxyError>
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomProxyProtocol {
|
extension RoomProxyProtocol {
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
emojiProvider: emojiProvider)
|
emojiProvider: emojiProvider)
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
coordinator.callback = { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .leftRoom:
|
||||||
|
self?.dismissRoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
detailNavigationStackCoordinator.setRootCoordinator(coordinator) { [weak self, roomIdentifier] in
|
detailNavigationStackCoordinator.setRootCoordinator(coordinator) { [weak self, roomIdentifier] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -192,6 +198,11 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func dismissRoom() {
|
||||||
|
detailNavigationStackCoordinator.popToRoot(animated: true)
|
||||||
|
navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Settings
|
// MARK: Settings
|
||||||
|
|
||||||
private func presentSettingsScreen() {
|
private func presentSettingsScreen() {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class MockScreen: Identifiable {
|
|||||||
case .roomPlainNoAvatar:
|
case .roomPlainNoAvatar:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Some room name", avatarURL: nil),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: nil)),
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -153,7 +153,7 @@ class MockScreen: Identifiable {
|
|||||||
case .roomEncryptedWithAvatar:
|
case .roomEncryptedWithAvatar:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Some room name", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -165,7 +165,7 @@ class MockScreen: Identifiable {
|
|||||||
let timelineController = MockRoomTimelineController()
|
let timelineController = MockRoomTimelineController()
|
||||||
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
|
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "New room", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "New room", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -180,7 +180,7 @@ class MockScreen: Identifiable {
|
|||||||
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk]
|
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk]
|
||||||
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
|
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Small timeline", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -195,7 +195,7 @@ class MockScreen: Identifiable {
|
|||||||
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
|
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
|
||||||
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
|
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Small timeline, paginating", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline, paginating", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -210,7 +210,7 @@ class MockScreen: Identifiable {
|
|||||||
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
|
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
|
||||||
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
|
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Large timeline", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -226,7 +226,7 @@ class MockScreen: Identifiable {
|
|||||||
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
|
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
|
||||||
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
|
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Large timeline", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -241,7 +241,7 @@ class MockScreen: Identifiable {
|
|||||||
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
|
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
|
||||||
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
|
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
|
||||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: MockRoomProxy(displayName: "Large timeline", avatarURL: URL.picturesDirectory),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider())
|
||||||
@@ -270,10 +270,10 @@ class MockScreen: Identifiable {
|
|||||||
return navigationSplitCoordinator
|
return navigationSplitCoordinator
|
||||||
case .roomDetailsScreen:
|
case .roomDetailsScreen:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let roomProxy = MockRoomProxy(id: "MockRoomIdentifier",
|
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||||
displayName: "Room",
|
displayName: "Room",
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
members: [.mockAlice, .mockBob, .mockCharlie])
|
members: [.mockAlice, .mockBob, .mockCharlie]))
|
||||||
let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider()))
|
mediaProvider: MockMediaProvider()))
|
||||||
@@ -281,13 +281,13 @@ class MockScreen: Identifiable {
|
|||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomDetailsScreenWithRoomAvatar:
|
case .roomDetailsScreenWithRoomAvatar:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let roomProxy = MockRoomProxy(id: "MockRoomIdentifier",
|
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||||
displayName: "Room",
|
displayName: "Room",
|
||||||
topic: "Bacon ipsum dolor amet commodo incididunt ribeye dolore cupidatat short ribs.",
|
topic: "Bacon ipsum dolor amet commodo incididunt ribeye dolore cupidatat short ribs.",
|
||||||
avatarURL: URL.picturesDirectory,
|
avatarURL: URL.picturesDirectory,
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
canonicalAlias: "#mock:room.org",
|
canonicalAlias: "#mock:room.org",
|
||||||
members: [.mockAlice, .mockBob, .mockCharlie])
|
members: [.mockAlice, .mockBob, .mockCharlie]))
|
||||||
let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider()))
|
mediaProvider: MockMediaProvider()))
|
||||||
@@ -301,7 +301,7 @@ class MockScreen: Identifiable {
|
|||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .reportContent:
|
case .reportContent:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: MockRoomProxy(displayName: "test")))
|
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: RoomProxyMock(with: .init(displayName: "test"))))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .startChat:
|
case .startChat:
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ output:
|
|||||||
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
||||||
args:
|
args:
|
||||||
automMockableTestableImports: []
|
automMockableTestableImports: []
|
||||||
autoMockableImports: [Combine, MatrixRustSDK]
|
autoMockableImports: [Combine, Foundation, MatrixRustSDK]
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bf58b16186bdaf165064a675a75a33a66a3ddd626c715e29bfd534844e209c1d
|
oid sha256:480187021f8337ffbc59d03b149d9aa27141b695d116daa311de27bca9b517c9
|
||||||
size 90332
|
size 95009
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:023baadc85550005385b8fd770a55bc2f082d52514837385855184758dbcdcf5
|
oid sha256:e035ab94494f59f9ecf1416f861b8d9bbee6477daad8a5cfdb107032cd14d322
|
||||||
size 116212
|
size 120849
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fbcfc6944b4c32583168276b6d64f68618dde1de251c772156918aa7b6644594
|
oid sha256:8eafd53e5f50f849a4a7da0bf436d0a532e725fe9adff3fc3fca788f5dac5edb
|
||||||
size 108453
|
size 115884
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e2d8a909c8ea5d9877ac951bd56334481f924b9a8ce1a495f7352164f1815d2a
|
oid sha256:1d6d23f2c8fb88c9baeab1181bbc1a59dc07fc36da4d9f3192f4e3d943e60a70
|
||||||
size 145566
|
size 150567
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5781e2acf57989a2091b6bcc1287a76966ab6b5ecea800b9e096338b31b60704
|
oid sha256:7533db27ed76004d975897520ea841421fb54aa77c9966330e41771ef7c18dba
|
||||||
size 87190
|
size 91562
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8da611272e84a28cc3a02b3ffdda36703a6ad8c9927ad9bd5bc412b95018bb16
|
oid sha256:7d267a4f41ee25c78e7beba699d89f67fa03da101a7b2cf30a973731cc38267f
|
||||||
size 112731
|
size 116394
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7bedda0ec007954c232d31713a9e6eb6adae2b561d2a856dede57e68c62dae68
|
oid sha256:d52f746ba47e31fb6aaa72dc7def6cd3ce9bde9a32eb2bbe7ad0da16d2249745
|
||||||
size 104960
|
size 109874
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:59a7327e8b127222fcf1cef19094af0cc10446d5e5d6de09cf4c0765ffa3c83e
|
oid sha256:f7d2db5f50221fcea17f3f0453b5098cae0dbdf16a1d531d34ad5656917481c4
|
||||||
size 142239
|
size 146955
|
||||||
|
|||||||
@@ -256,6 +256,32 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
|||||||
waitForExpectations(timeout: 1.0)
|
waitForExpectations(timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSetRootDetailToNilAfterPoppingToRoot() {
|
||||||
|
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SomeTestCoordinator())
|
||||||
|
let sidebarCoordinator = NavigationStackCoordinator()
|
||||||
|
sidebarCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||||
|
|
||||||
|
let detailCoordinator = NavigationStackCoordinator()
|
||||||
|
detailCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||||
|
detailCoordinator.push(SomeTestCoordinator())
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||||
|
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||||
|
|
||||||
|
let expectation = expectation(description: "Details coordinator should be nil, and the compact layout revert to the sidebar root")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
detailCoordinator.popToRoot(animated: true)
|
||||||
|
self.navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
XCTAssertNil(self.navigationSplitCoordinator.detailCoordinator)
|
||||||
|
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||||
|
XCTAssertTrue(self.navigationSplitCoordinator.compactLayoutStackModules.isEmpty)
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waitForExpectations(timeout: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import XCTest
|
|||||||
@MainActor
|
@MainActor
|
||||||
class ReportContentScreenViewModelTests: XCTestCase {
|
class ReportContentScreenViewModelTests: XCTestCase {
|
||||||
func testInitialState() {
|
func testInitialState() {
|
||||||
let viewModel = ReportContentViewModel(itemID: "test-id", roomProxy: MockRoomProxy(displayName: "test"))
|
let viewModel = ReportContentViewModel(itemID: "test-id", roomProxy: RoomProxyMock(with: .init(displayName: "test")))
|
||||||
let context = viewModel.context
|
let context = viewModel.context
|
||||||
|
|
||||||
XCTAssertEqual(context.reasonText, "")
|
XCTAssertEqual(context.reasonText, "")
|
||||||
|
|||||||
@@ -19,4 +19,65 @@ import XCTest
|
|||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class RoomDetailsScreenViewModelTests: XCTestCase { }
|
class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||||
|
var viewModel: RoomDetailsViewModelProtocol!
|
||||||
|
var roomProxyMock: RoomProxyMock!
|
||||||
|
var context: RoomDetailsViewModelType.Context { viewModel.context }
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test"))
|
||||||
|
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLeaveRoomTappedWhenPublic() async {
|
||||||
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: [.mockBob, .mockAlice]))
|
||||||
|
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
context.send(viewAction: .processTapLeave)
|
||||||
|
await Task.yield()
|
||||||
|
XCTAssertEqual(context.leaveRoomAlertItem?.state, .public)
|
||||||
|
XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, ElementL10n.roomDetailsLeaveRoomAlertSubtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLeavRoomTappedWhenRoomNotPublic() async {
|
||||||
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: [.mockBob, .mockAlice]))
|
||||||
|
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
context.send(viewAction: .processTapLeave)
|
||||||
|
await Task.yield()
|
||||||
|
XCTAssertEqual(context.leaveRoomAlertItem?.state, .private)
|
||||||
|
XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, ElementL10n.roomDetailsLeavePrivateRoomAlertSubtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLeaveRoomTappedWithLessThanTwoMembers() async {
|
||||||
|
context.send(viewAction: .processTapLeave)
|
||||||
|
await Task.yield()
|
||||||
|
XCTAssertEqual(context.leaveRoomAlertItem?.state, .empty)
|
||||||
|
XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, ElementL10n.roomDetailsLeaveEmptyRoomAlertSubtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLeaveRoomSuccess() async {
|
||||||
|
roomProxyMock.leaveRoomClosure = {
|
||||||
|
.success(())
|
||||||
|
}
|
||||||
|
viewModel.callback = { action in
|
||||||
|
switch action {
|
||||||
|
case .leftRoom:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
XCTFail("leftRoom expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.send(viewAction: .confirmLeave)
|
||||||
|
await Task.yield()
|
||||||
|
XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLeaveRoomError() async {
|
||||||
|
roomProxyMock.leaveRoomClosure = {
|
||||||
|
.failure(.failedLeavingRoom)
|
||||||
|
}
|
||||||
|
context.send(viewAction: .confirmLeave)
|
||||||
|
await Task.yield()
|
||||||
|
XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
|
||||||
|
XCTAssertNotNil(context.alertInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -97,7 +97,8 @@ end
|
|||||||
|
|
||||||
lane :unit_tests do
|
lane :unit_tests do
|
||||||
run_tests(
|
run_tests(
|
||||||
scheme: "UnitTests"
|
scheme: "UnitTests",
|
||||||
|
result_bundle: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
slather(
|
slather(
|
||||||
|
|||||||
Reference in New Issue
Block a user