mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Various UI tweaks (#1112)
* Tweak invites button frame and padding * Implement persistent selections on the room list * Stop showing rooms when focusing the room list search field with an empty query * Tweaks after code review * Improve invite button UI * Fix unit tests
This commit is contained in:
@@ -417,6 +417,7 @@
|
|||||||
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
|
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
|
||||||
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
||||||
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; };
|
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; };
|
||||||
|
A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */; };
|
||||||
A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; };
|
A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; };
|
||||||
A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; };
|
A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; };
|
||||||
A1D4033881320C9EB88196E6 /* ServerConfirmationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */; };
|
A1D4033881320C9EB88196E6 /* ServerConfirmationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */; };
|
||||||
@@ -565,6 +566,7 @@
|
|||||||
D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; };
|
D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; };
|
||||||
D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; };
|
D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; };
|
||||||
D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; };
|
D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; };
|
||||||
|
D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */; };
|
||||||
D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; };
|
D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; };
|
||||||
D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; };
|
D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; };
|
||||||
D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; };
|
D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; };
|
||||||
@@ -1210,6 +1212,7 @@
|
|||||||
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
||||||
C733D11B421CFE3A657EF230 /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = "<group>"; };
|
C733D11B421CFE3A657EF230 /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = "<group>"; };
|
||||||
C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = "<group>"; };
|
||||||
C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
|
C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
|
||||||
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
||||||
C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = "<group>"; };
|
C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = "<group>"; };
|
||||||
@@ -1316,6 +1319,7 @@
|
|||||||
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
|
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
|
||||||
F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||||
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = "<group>"; };
|
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = "<group>"; };
|
||||||
|
F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUserMenuButton.swift; sourceTree = "<group>"; };
|
||||||
F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = "<group>"; };
|
F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsView.swift; sourceTree = "<group>"; };
|
F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsView.swift; sourceTree = "<group>"; };
|
||||||
F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -2008,6 +2012,8 @@
|
|||||||
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */,
|
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */,
|
||||||
24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */,
|
24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */,
|
||||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */,
|
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */,
|
||||||
|
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */,
|
||||||
|
F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3979,6 +3985,8 @@
|
|||||||
64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */,
|
64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */,
|
||||||
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */,
|
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */,
|
||||||
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */,
|
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */,
|
||||||
|
A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */,
|
||||||
|
D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */,
|
||||||
DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */,
|
DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */,
|
||||||
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */,
|
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */,
|
||||||
03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */,
|
03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */,
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
|
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
|
||||||
private let detailNavigationStackCoordinator: NavigationStackCoordinator
|
private let detailNavigationStackCoordinator: NavigationStackCoordinator
|
||||||
|
|
||||||
|
private let selectedRoomSubject = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
|
||||||
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
|
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
init(userSession: UserSessionProtocol,
|
init(userSession: UserSessionProtocol,
|
||||||
@@ -109,7 +111,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||||
private func setupStateMachine() {
|
private func setupStateMachine() {
|
||||||
stateMachine.addTransitionHandler { [weak self] context in
|
stateMachine.addTransitionHandler { [weak self] context in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -160,6 +162,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stateMachine.addTransitionHandler { [weak self] context in
|
||||||
|
switch context.toState {
|
||||||
|
case .roomList(let selectedRoomId):
|
||||||
|
self?.selectedRoomSubject.send(selectedRoomId)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stateMachine.addErrorHandler { context in
|
stateMachine.addErrorHandler { context in
|
||||||
fatalError("Failed transition with context: \(context)")
|
fatalError("Failed transition with context: \(context)")
|
||||||
}
|
}
|
||||||
@@ -170,7 +181,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||||
attributedStringBuilder: AttributedStringBuilder(),
|
attributedStringBuilder: AttributedStringBuilder(),
|
||||||
bugReportService: bugReportService,
|
bugReportService: bugReportService,
|
||||||
navigationStackCoordinator: detailNavigationStackCoordinator)
|
navigationStackCoordinator: detailNavigationStackCoordinator,
|
||||||
|
selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher())
|
||||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
coordinator.callback = { [weak self] action in
|
coordinator.callback = { [weak self] action in
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// A view modifier that applies a shimmering effect to the view.
|
/// A view modifier that applies a shimmering effect to the view.
|
||||||
@@ -72,12 +73,13 @@ extension View {
|
|||||||
struct ShimmerOverlay_Previews: PreviewProvider {
|
struct ShimmerOverlay_Previews: PreviewProvider {
|
||||||
static let viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: ""),
|
static let viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: ""),
|
||||||
mediaProvider: MockMediaProvider()),
|
mediaProvider: MockMediaProvider()),
|
||||||
attributedStringBuilder: AttributedStringBuilder())
|
attributedStringBuilder: AttributedStringBuilder(),
|
||||||
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher())
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ForEach(0...8, id: \.self) { _ in
|
ForEach(0...8, id: \.self) { _ in
|
||||||
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context)
|
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct HomeScreenCoordinatorParameters {
|
|||||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||||
let bugReportService: BugReportServiceProtocol
|
let bugReportService: BugReportServiceProtocol
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator
|
let navigationStackCoordinator: NavigationStackCoordinator
|
||||||
|
let selectedRoomPublisher: CurrentValuePublisher<String?, Never>
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HomeScreenCoordinatorAction {
|
enum HomeScreenCoordinatorAction {
|
||||||
@@ -48,7 +49,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = HomeScreenViewModel(userSession: parameters.userSession,
|
viewModel = HomeScreenViewModel(userSession: parameters.userSession,
|
||||||
attributedStringBuilder: parameters.attributedStringBuilder)
|
attributedStringBuilder: parameters.attributedStringBuilder,
|
||||||
|
selectedRoomPublisher: parameters.selectedRoomPublisher)
|
||||||
|
|
||||||
viewModel.callback = { [weak self] action in
|
viewModel.callback = { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ struct HomeScreenViewState: BindableState {
|
|||||||
var hasPendingInvitations = false
|
var hasPendingInvitations = false
|
||||||
var hasUnreadPendingInvitations = false
|
var hasUnreadPendingInvitations = false
|
||||||
|
|
||||||
|
var selectedRoomID: String?
|
||||||
|
|
||||||
var visibleRooms: [HomeScreenRoom] {
|
var visibleRooms: [HomeScreenRoom] {
|
||||||
if roomListMode == .skeletons {
|
if roomListMode == .skeletons {
|
||||||
return placeholderRooms
|
return placeholderRooms
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
init(userSession: UserSessionProtocol,
|
||||||
|
attributedStringBuilder: AttributedStringBuilderProtocol,
|
||||||
|
selectedRoomPublisher: CurrentValuePublisher<String?, Never>) {
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
self.attributedStringBuilder = attributedStringBuilder
|
self.attributedStringBuilder = attributedStringBuilder
|
||||||
|
|
||||||
@@ -59,6 +61,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
.weakAssign(to: \.state.userAvatarURL, on: self)
|
.weakAssign(to: \.state.userAvatarURL, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
selectedRoomPublisher
|
||||||
|
.weakAssign(to: \.state.selectedRoomID, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
guard let roomSummaryProvider, let inviteSummaryProvider else {
|
guard let roomSummaryProvider, let inviteSummaryProvider else {
|
||||||
MXLog.error("Room summary provider unavailable")
|
MXLog.error("Room summary provider unavailable")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Compound
|
import Compound
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -25,8 +26,8 @@ struct HomeScreen: View {
|
|||||||
@ObservedObject var context: HomeScreenViewModel.Context
|
@ObservedObject var context: HomeScreenViewModel.Context
|
||||||
|
|
||||||
@State private var scrollViewAdapter = ScrollViewAdapter()
|
@State private var scrollViewAdapter = ScrollViewAdapter()
|
||||||
@State private var showingLogoutConfirmation = false
|
|
||||||
@State private var showingBottomToolbar = true
|
@State private var showingBottomToolbar = true
|
||||||
|
@State private var isSearching = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -34,18 +35,18 @@ struct HomeScreen: View {
|
|||||||
sessionVerificationBanner
|
sessionVerificationBanner
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.viewState.hasPendingInvitations {
|
if context.viewState.hasPendingInvitations, !isSearching {
|
||||||
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
||||||
context.send(viewAction: .selectInvites)
|
context.send(viewAction: .selectInvites)
|
||||||
}
|
}
|
||||||
.padding(.trailing, 16)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
.padding(.vertical, -8.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.viewState.roomListMode == .skeletons {
|
if context.viewState.roomListMode == .skeletons {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(context.viewState.visibleRooms) { room in
|
ForEach(context.viewState.visibleRooms) { room in
|
||||||
HomeScreenRoomCell(room: room, context: context)
|
HomeScreenRoomCell(room: room, context: context, isSelected: false)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,29 +54,7 @@ struct HomeScreen: View {
|
|||||||
.disabled(true)
|
.disabled(true)
|
||||||
} else {
|
} else {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(context.viewState.visibleRooms) { room in
|
HomeScreenRoomList(context: context, isSearching: $isSearching)
|
||||||
Group {
|
|
||||||
if room.isPlaceholder {
|
|
||||||
HomeScreenRoomCell(room: room, context: context)
|
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
} else {
|
|
||||||
HomeScreenRoomCell(room: room, context: context)
|
|
||||||
.contextMenu {
|
|
||||||
Button {
|
|
||||||
context.send(viewAction: .showRoomDetails(roomIdentifier: room.id))
|
|
||||||
} label: {
|
|
||||||
Label(L10n.commonSettings, systemImage: "gearshape")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(role: .destructive) {
|
|
||||||
context.send(viewAction: .leaveRoom(roomIdentifier: room.id))
|
|
||||||
} label: {
|
|
||||||
Label(L10n.actionLeaveRoom, systemImage: "rectangle.portrait.and.arrow.right")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.searchable(text: $context.searchQuery)
|
.searchable(text: $context.searchQuery)
|
||||||
.compoundSearchField()
|
.compoundSearchField()
|
||||||
@@ -132,7 +111,7 @@ struct HomeScreen: View {
|
|||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
private var toolbar: some ToolbarContent {
|
private var toolbar: some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
userMenuButton
|
HomeScreenUserMenuButton(context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItemGroup(placement: .bottomBar) {
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
@@ -141,50 +120,10 @@ struct HomeScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var userMenuButton: some View {
|
|
||||||
Menu {
|
|
||||||
Section {
|
|
||||||
Button(action: settings) {
|
|
||||||
Label(L10n.commonSettings, systemImage: "gearshape")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Section {
|
|
||||||
MatrixUserShareLink(userID: context.viewState.userID) {
|
|
||||||
Label(L10n.actionInvite, systemImage: "square.and.arrow.up")
|
|
||||||
}
|
|
||||||
Button(action: feedback) {
|
|
||||||
Label(L10n.commonReportABug, systemImage: "ladybug")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Section {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
showingLogoutConfirmation = true
|
|
||||||
} label: {
|
|
||||||
Label(L10n.screenSignoutPreferenceItem, systemImage: "rectangle.portrait.and.arrow.right")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
|
||||||
name: context.viewState.userDisplayName,
|
|
||||||
contentID: context.viewState.userID,
|
|
||||||
avatarSize: .user(on: .home),
|
|
||||||
imageProvider: context.imageProvider)
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar)
|
|
||||||
}
|
|
||||||
.alert(L10n.screenSignoutConfirmationDialogTitle,
|
|
||||||
isPresented: $showingLogoutConfirmation) {
|
|
||||||
Button(L10n.screenSignoutConfirmationDialogSubmit,
|
|
||||||
role: .destructive,
|
|
||||||
action: signOut)
|
|
||||||
} message: {
|
|
||||||
Text(L10n.screenSignoutConfirmationDialogContent)
|
|
||||||
}
|
|
||||||
.accessibilityLabel(L10n.a11yUserMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var newRoomButton: some View {
|
private var newRoomButton: some View {
|
||||||
Button(action: startChat) {
|
Button {
|
||||||
|
context.send(viewAction: .startChat)
|
||||||
|
} label: {
|
||||||
Image(systemName: "square.and.pencil")
|
Image(systemName: "square.and.pencil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,22 +164,6 @@ struct HomeScreen: View {
|
|||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func settings() {
|
|
||||||
context.send(viewAction: .userMenu(action: .settings))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startChat() {
|
|
||||||
context.send(viewAction: .startChat)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func feedback() {
|
|
||||||
context.send(viewAction: .userMenu(action: .feedback))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func signOut() {
|
|
||||||
context.send(viewAction: .userMenu(action: .signOut))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateVisibleRange() {
|
private func updateVisibleRange() {
|
||||||
guard let scrollView = scrollViewAdapter.scrollView,
|
guard let scrollView = scrollViewAdapter.scrollView,
|
||||||
context.viewState.visibleRooms.count > 0 else {
|
context.viewState.visibleRooms.count > 0 else {
|
||||||
@@ -292,7 +215,8 @@ struct HomeScreen_Previews: PreviewProvider {
|
|||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||||
attributedStringBuilder: AttributedStringBuilder())
|
attributedStringBuilder: AttributedStringBuilder(),
|
||||||
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher())
|
||||||
|
|
||||||
return NavigationStack {
|
return NavigationStack {
|
||||||
HomeScreen(context: viewModel.context)
|
HomeScreen(context: viewModel.context)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct HomeScreenInvitesButton: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8.0) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.foregroundColor(.compound.textPrimary)
|
.foregroundColor(.compound.textPrimary)
|
||||||
.font(.compound.bodyMDSemibold)
|
.font(.compound.bodyMDSemibold)
|
||||||
@@ -34,7 +34,9 @@ struct HomeScreenInvitesButton: View {
|
|||||||
badge
|
badge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.trailing, 16.0)
|
||||||
|
.padding(.leading, 64.0)
|
||||||
|
.padding(.vertical, 8.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HomeScreenRoomCell: View {
|
struct HomeScreenRoomCell: View {
|
||||||
@@ -22,6 +23,7 @@ struct HomeScreenRoomCell: View {
|
|||||||
|
|
||||||
let room: HomeScreenRoom
|
let room: HomeScreenRoom
|
||||||
let context: HomeScreenViewModel.Context
|
let context: HomeScreenViewModel.Context
|
||||||
|
let isSelected: Bool
|
||||||
|
|
||||||
private let verticalInsets = 12.0
|
private let verticalInsets = 12.0
|
||||||
private let horizontalInsets = 16.0
|
private let horizontalInsets = 16.0
|
||||||
@@ -47,7 +49,7 @@ struct HomeScreenRoomCell: View {
|
|||||||
.padding(.horizontal, horizontalInsets)
|
.padding(.horizontal, horizontalInsets)
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
}
|
}
|
||||||
.buttonStyle(HomeScreenRoomCellButtonStyle(isSelected: false))
|
.buttonStyle(HomeScreenRoomCellButtonStyle(isSelected: isSelected))
|
||||||
.accessibilityIdentifier(A11yIdentifiers.homeScreen.roomName(room.name))
|
.accessibilityIdentifier(A11yIdentifiers.homeScreen.roomName(room.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +151,9 @@ struct HomeScreenRoomCellButtonStyle: ButtonStyle {
|
|||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
.background(configuration.isPressed || isSelected ? Color.compound.bgSubtleSecondary : .clear)
|
.background(isSelected ? Color.compound.bgSubtleSecondary : Color.compound.bgCanvasDefault)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
.animation(.elementDefault, value: isSelected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +174,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider {
|
|||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||||
attributedStringBuilder: AttributedStringBuilder())
|
attributedStringBuilder: AttributedStringBuilder(),
|
||||||
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher())
|
||||||
|
|
||||||
let rooms: [HomeScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary -> HomeScreenRoom? in
|
let rooms: [HomeScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary -> HomeScreenRoom? in
|
||||||
switch summary {
|
switch summary {
|
||||||
@@ -190,14 +194,14 @@ struct HomeScreenRoomCell_Previews: PreviewProvider {
|
|||||||
|
|
||||||
return VStack(spacing: 0) {
|
return VStack(spacing: 0) {
|
||||||
ForEach(rooms) { room in
|
ForEach(rooms) { room in
|
||||||
HomeScreenRoomCell(room: room, context: viewModel.context)
|
HomeScreenRoomCell(room: room, context: viewModel.context, isSelected: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context)
|
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context)
|
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context)
|
HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct HomeScreenRoomList: View {
|
||||||
|
@Environment(\.isSearching) var isSearchFieldFocused
|
||||||
|
|
||||||
|
@ObservedObject var context: HomeScreenViewModel.Context
|
||||||
|
@Binding var isSearching: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content
|
||||||
|
.onChange(of: isSearchFieldFocused) { isSearching = $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
if isSearchFieldFocused, context.searchQuery.count == 0 {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
|
ForEach(context.viewState.visibleRooms) { room in
|
||||||
|
if room.isPlaceholder {
|
||||||
|
HomeScreenRoomCell(room: room, context: context, isSelected: false)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
} else {
|
||||||
|
let isSelected = context.viewState.selectedRoomID == room.id
|
||||||
|
HomeScreenRoomCell(room: room, context: context, isSelected: isSelected)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .showRoomDetails(roomIdentifier: room.id))
|
||||||
|
} label: {
|
||||||
|
Label(L10n.commonSettings, systemImage: "gearshape")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(role: .destructive) {
|
||||||
|
context.send(viewAction: .leaveRoom(roomIdentifier: room.id))
|
||||||
|
} label: {
|
||||||
|
Label(L10n.actionLeaveRoom, systemImage: "rectangle.portrait.and.arrow.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct HomeScreenUserMenuButton: View {
|
||||||
|
@State private var showingLogoutConfirmation = false
|
||||||
|
|
||||||
|
let context: HomeScreenViewModel.Context
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Menu {
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .userMenu(action: .settings))
|
||||||
|
} label: {
|
||||||
|
Label(L10n.commonSettings, systemImage: "gearshape")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
MatrixUserShareLink(userID: context.viewState.userID) {
|
||||||
|
Label(L10n.actionInvite, systemImage: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .userMenu(action: .feedback))
|
||||||
|
} label: {
|
||||||
|
Label(L10n.commonReportABug, systemImage: "ladybug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
showingLogoutConfirmation = true
|
||||||
|
} label: {
|
||||||
|
Label(L10n.screenSignoutPreferenceItem, systemImage: "rectangle.portrait.and.arrow.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
||||||
|
name: context.viewState.userDisplayName,
|
||||||
|
contentID: context.viewState.userID,
|
||||||
|
avatarSize: .user(on: .home),
|
||||||
|
imageProvider: context.imageProvider)
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar)
|
||||||
|
}
|
||||||
|
.alert(L10n.screenSignoutConfirmationDialogTitle,
|
||||||
|
isPresented: $showingLogoutConfirmation) {
|
||||||
|
Button(L10n.screenSignoutConfirmationDialogSubmit,
|
||||||
|
role: .destructive) {
|
||||||
|
context.send(viewAction: .userMenu(action: .signOut))
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(L10n.screenSignoutConfirmationDialogContent)
|
||||||
|
}
|
||||||
|
.accessibilityLabel(L10n.a11yUserMenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,7 +117,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
|
||||||
private func setupSubscriptions() {
|
private func setupSubscriptions() {
|
||||||
timelineController.callbacks
|
timelineController.callbacks
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ class MockScreen: Identifiable {
|
|||||||
let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session,
|
let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session,
|
||||||
attributedStringBuilder: AttributedStringBuilder(),
|
attributedStringBuilder: AttributedStringBuilder(),
|
||||||
bugReportService: BugReportServiceMock(),
|
bugReportService: BugReportServiceMock(),
|
||||||
navigationStackCoordinator: navigationStackCoordinator))
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher()))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .settings:
|
case .settings:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
@@ -31,7 +32,8 @@ class HomeScreenViewModelTests: XCTestCase {
|
|||||||
clientProxy = MockClientProxy(userID: "@mock:client.com")
|
clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||||
viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: clientProxy,
|
viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: clientProxy,
|
||||||
mediaProvider: MockMediaProvider()),
|
mediaProvider: MockMediaProvider()),
|
||||||
attributedStringBuilder: AttributedStringBuilder())
|
attributedStringBuilder: AttributedStringBuilder(),
|
||||||
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSelectRoom() async throws {
|
func testSelectRoom() async throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user