mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Invites list (#787)
* Setup invites SS window * Add invites label in the home screen * Add empty invtes list * Setup navigation to invites list * Inject invitesSummaryProvider * Show invites * Add InviteCell * Refine InviteCell UI * Push invites * Amend SS configuration * Add inviter in RoomProxyProtocol * Add Invite model type * Improve InviteCell * Fix media provider injection * Refine InviteCell * Refine invite cell * Add invites feature flag * Try different SS config for invites * Regiester invites view in configureViewsPostInitialSync * Cleanup * Start tests * Cleanup code * Add changelog.d file * Add tests * Handle canonical alias * Add InvitesListScreen previews * Add localisations and improve UI tests * Add reference screenshots * Cleanup code * Fix UT build errors * Refactor InvitesList -> Invites * Apply pr comments * Remove reduntant @MainActor * Naming cleanup * Fix InvitesScreenCell.title * Add accept/decline InvitesViewAction * Revert Package.resolved * Add warning in InvitesScreenCell * Improve PlaceholderAvatarImage * Record snapshots again * Refactor Invite -> InvitesRoomDeatils * Rename identifier in PlaceholderAvatarImage
This commit is contained in:
@@ -30,6 +30,7 @@ final class AppSettings: ObservableObject {
|
|||||||
case startChatFlowEnabled = "showStartChatFlow"
|
case startChatFlowEnabled = "showStartChatFlow"
|
||||||
case startChatUserSuggestionsEnabled
|
case startChatUserSuggestionsEnabled
|
||||||
case mediaUploadingFlowEnabled
|
case mediaUploadingFlowEnabled
|
||||||
|
case invitesFlowEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
@@ -172,4 +173,9 @@ final class AppSettings: ObservableObject {
|
|||||||
|
|
||||||
@UserSetting(key: UserDefaultsKeys.mediaUploadingFlowEnabled.rawValue, defaultValue: false, persistIn: nil)
|
@UserSetting(key: UserDefaultsKeys.mediaUploadingFlowEnabled.rawValue, defaultValue: false, persistIn: nil)
|
||||||
var mediaUploadingFlowEnabled
|
var mediaUploadingFlowEnabled
|
||||||
|
|
||||||
|
// MARK: Invites
|
||||||
|
|
||||||
|
@UserSetting(key: UserDefaultsKeys.invitesFlowEnabled.rawValue, defaultValue: false, persistIn: store)
|
||||||
|
var invitesFlowEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -481,6 +481,23 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
updateMembersCallsCount += 1
|
updateMembersCallsCount += 1
|
||||||
await updateMembersClosure?()
|
await updateMembersClosure?()
|
||||||
}
|
}
|
||||||
|
//MARK: - inviter
|
||||||
|
|
||||||
|
var inviterCallsCount = 0
|
||||||
|
var inviterCalled: Bool {
|
||||||
|
return inviterCallsCount > 0
|
||||||
|
}
|
||||||
|
var inviterReturnValue: RoomMemberProxyProtocol?
|
||||||
|
var inviterClosure: (() async -> RoomMemberProxyProtocol?)?
|
||||||
|
|
||||||
|
func inviter() async -> RoomMemberProxyProtocol? {
|
||||||
|
inviterCallsCount += 1
|
||||||
|
if let inviterClosure = inviterClosure {
|
||||||
|
return await inviterClosure()
|
||||||
|
} else {
|
||||||
|
return inviterReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
||||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ struct A11yIdentifiers {
|
|||||||
static let softLogoutScreen = SoftLogoutScreen()
|
static let softLogoutScreen = SoftLogoutScreen()
|
||||||
static let startChatScreen = StartChatScreen()
|
static let startChatScreen = StartChatScreen()
|
||||||
static let roomMemberDetailsScreen = RoomMemberDetailsScreen()
|
static let roomMemberDetailsScreen = RoomMemberDetailsScreen()
|
||||||
|
static let invitesScreen = InvitesScreen()
|
||||||
|
|
||||||
struct BugReportScreen {
|
struct BugReportScreen {
|
||||||
let report = "bug_report-report"
|
let report = "bug_report-report"
|
||||||
@@ -114,4 +115,8 @@ struct A11yIdentifiers {
|
|||||||
let ignore = "room_member_details-ignore"
|
let ignore = "room_member_details-ignore"
|
||||||
let unignore = "room_member_details-unignore"
|
let unignore = "room_member_details-unignore"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct InvitesScreen {
|
||||||
|
let noInvites = "invites-no_invites"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,16 +23,19 @@ struct PlaceholderAvatarImage: View {
|
|||||||
private let contentID: String?
|
private let contentID: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
GeometryReader { geometry in
|
||||||
|
ZStack(alignment: .center) {
|
||||||
bgColor
|
bgColor
|
||||||
|
|
||||||
// This text's frame doesn't look right when redacted
|
// This text's frame doesn't look right when redacted
|
||||||
if redactionReasons != .placeholder {
|
if redactionReasons != .placeholder {
|
||||||
Text(textForImage)
|
Text(textForImage)
|
||||||
.padding(4)
|
.padding(geometry.size.width <= 30 ? 0 : 4)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 200).weight(.semibold))
|
.font(.system(size: 200).weight(.semibold))
|
||||||
.minimumScaleFactor(0.001)
|
.minimumScaleFactor(0.001)
|
||||||
|
.frame(alignment: .center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.aspectRatio(1, contentMode: .fill)
|
.aspectRatio(1, contentMode: .fill)
|
||||||
@@ -71,6 +74,10 @@ struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
|||||||
PlaceholderAvatarImage(name: nil, contentID: "@userid3:matrix.org")
|
PlaceholderAvatarImage(name: nil, contentID: "@userid3:matrix.org")
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.frame(width: 150, height: 100)
|
.frame(width: 150, height: 100)
|
||||||
|
|
||||||
|
PlaceholderAvatarImage(name: nil, contentID: "@fooserid:matrix.org")
|
||||||
|
.clipShape(Circle())
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct DeveloperOptionsScreenViewStateBindings {
|
|||||||
var startChatFlowEnabled: Bool
|
var startChatFlowEnabled: Bool
|
||||||
var startChatUserSuggestionsEnabled: Bool
|
var startChatUserSuggestionsEnabled: Bool
|
||||||
var mediaUploadFlowEnabled: Bool
|
var mediaUploadFlowEnabled: Bool
|
||||||
|
var invitesFlowEnabled: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DeveloperOptionsScreenViewAction {
|
enum DeveloperOptionsScreenViewAction {
|
||||||
@@ -34,4 +35,5 @@ enum DeveloperOptionsScreenViewAction {
|
|||||||
case changedStartChatFlowEnabled
|
case changedStartChatFlowEnabled
|
||||||
case changedStartChatUserSuggestionsEnabled
|
case changedStartChatUserSuggestionsEnabled
|
||||||
case changedMediaUploadFlowEnabled
|
case changedMediaUploadFlowEnabled
|
||||||
|
case changedInvitesFlowEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -25,7 +25,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
||||||
startChatFlowEnabled: ServiceLocator.shared.settings.startChatFlowEnabled,
|
startChatFlowEnabled: ServiceLocator.shared.settings.startChatFlowEnabled,
|
||||||
startChatUserSuggestionsEnabled: ServiceLocator.shared.settings.startChatUserSuggestionsEnabled,
|
startChatUserSuggestionsEnabled: ServiceLocator.shared.settings.startChatUserSuggestionsEnabled,
|
||||||
mediaUploadFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled)
|
mediaUploadFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled,
|
||||||
|
invitesFlowEnabled: ServiceLocator.shared.settings.invitesFlowEnabled)
|
||||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||||
|
|
||||||
super.init(initialViewState: state)
|
super.init(initialViewState: state)
|
||||||
@@ -45,6 +46,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
||||||
case .changedMediaUploadFlowEnabled:
|
case .changedMediaUploadFlowEnabled:
|
||||||
ServiceLocator.shared.settings.mediaUploadingFlowEnabled = state.bindings.mediaUploadFlowEnabled
|
ServiceLocator.shared.settings.mediaUploadingFlowEnabled = state.bindings.mediaUploadFlowEnabled
|
||||||
|
case .changedInvitesFlowEnabled:
|
||||||
|
ServiceLocator.shared.settings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ struct DeveloperOptionsScreen: View {
|
|||||||
.onChange(of: context.mediaUploadFlowEnabled) { _ in
|
.onChange(of: context.mediaUploadFlowEnabled) { _ in
|
||||||
context.send(viewAction: .changedMediaUploadFlowEnabled)
|
context.send(viewAction: .changedMediaUploadFlowEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $context.invitesFlowEnabled) {
|
||||||
|
Text("Show Invites flow")
|
||||||
|
}
|
||||||
|
.onChange(of: context.invitesFlowEnabled) { _ in
|
||||||
|
context.send(viewAction: .changedInvitesFlowEnabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ enum HomeScreenCoordinatorAction {
|
|||||||
case presentFeedbackScreen
|
case presentFeedbackScreen
|
||||||
case presentSessionVerificationScreen
|
case presentSessionVerificationScreen
|
||||||
case presentStartChatScreen
|
case presentStartChatScreen
|
||||||
|
case presentInvitesScreen
|
||||||
case signOut
|
case signOut
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.callback?(.signOut)
|
self.callback?(.signOut)
|
||||||
case .presentStartChatScreen:
|
case .presentStartChatScreen:
|
||||||
self.callback?(.presentStartChatScreen)
|
self.callback?(.presentStartChatScreen)
|
||||||
|
case .presentInvitesScreen:
|
||||||
|
self.callback?(.presentInvitesScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum HomeScreenViewModelAction {
|
|||||||
case presentSettingsScreen
|
case presentSettingsScreen
|
||||||
case presentFeedbackScreen
|
case presentFeedbackScreen
|
||||||
case presentStartChatScreen
|
case presentStartChatScreen
|
||||||
|
case presentInvitesScreen
|
||||||
case signOut
|
case signOut
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ enum HomeScreenViewAction {
|
|||||||
case verifySession
|
case verifySession
|
||||||
case skipSessionVerification
|
case skipSessionVerification
|
||||||
case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool)
|
case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool)
|
||||||
|
case selectInvites
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HomeScreenRoomListMode: CustomStringConvertible {
|
enum HomeScreenRoomListMode: CustomStringConvertible {
|
||||||
@@ -70,6 +72,8 @@ struct HomeScreenViewState: BindableState {
|
|||||||
/// The URL that will be shared when inviting friends to use the app.
|
/// The URL that will be shared when inviting friends to use the app.
|
||||||
let invitePermalink: URL?
|
let invitePermalink: URL?
|
||||||
|
|
||||||
|
var hasPendingInvitations = false
|
||||||
|
|
||||||
var startChatFlowEnabled: Bool {
|
var startChatFlowEnabled: Bool {
|
||||||
ServiceLocator.shared.settings.startChatFlowEnabled
|
ServiceLocator.shared.settings.startChatFlowEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let visibleRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
private let visibleRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
private let allRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
private let allRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
|
private let invitesSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||||
|
|
||||||
private var visibleItemRangeObservationToken: AnyCancellable?
|
private var visibleItemRangeObservationToken: AnyCancellable?
|
||||||
@@ -37,6 +38,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
|
|
||||||
visibleRoomsSummaryProvider = userSession.clientProxy.visibleRoomsSummaryProvider
|
visibleRoomsSummaryProvider = userSession.clientProxy.visibleRoomsSummaryProvider
|
||||||
allRoomsSummaryProvider = userSession.clientProxy.allRoomsSummaryProvider
|
allRoomsSummaryProvider = userSession.clientProxy.allRoomsSummaryProvider
|
||||||
|
invitesSummaryProvider = userSession.clientProxy.invitesSummaryProvider
|
||||||
|
|
||||||
let invitePermalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userSession.userID)
|
let invitePermalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userSession.userID)
|
||||||
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID, invitePermalink: invitePermalink),
|
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID, invitePermalink: invitePermalink),
|
||||||
@@ -60,7 +62,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
.weakAssign(to: \.state.userAvatarURL, on: self)
|
.weakAssign(to: \.state.userAvatarURL, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
guard let visibleRoomsSummaryProvider, let allRoomsSummaryProvider else {
|
guard let visibleRoomsSummaryProvider, let allRoomsSummaryProvider, let invitesSummaryProvider else {
|
||||||
MXLog.error("Room summary provider unavailable")
|
MXLog.error("Room summary provider unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -132,6 +134,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
invitesSummaryProvider.countPublisher
|
||||||
|
.map { $0 > 0 }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.weakAssign(to: \.state.hasPendingInvitations, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
updateRooms()
|
updateRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +166,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
visibleItemRangePublisher.send((range, isScrolling))
|
visibleItemRangePublisher.send((range, isScrolling))
|
||||||
case .startChat:
|
case .startChat:
|
||||||
callback?(.presentStartChatScreen)
|
callback?(.presentStartChatScreen)
|
||||||
|
case .selectInvites:
|
||||||
|
callback?(.presentInvitesScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ struct HomeScreen: View {
|
|||||||
sessionVerificationBanner
|
sessionVerificationBanner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if context.viewState.hasPendingInvitations, ServiceLocator.shared.settings.invitesFlowEnabled {
|
||||||
|
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: true) {
|
||||||
|
context.send(viewAction: .selectInvites)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -240,7 +248,7 @@ struct HomeScreen: View {
|
|||||||
struct HomeScreen_Previews: PreviewProvider {
|
struct HomeScreen_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
body(.loading)
|
body(.loading)
|
||||||
body(.loaded)
|
body(.loaded(.mockRooms))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func body(_ state: MockRoomSummaryProviderState) -> some View {
|
static func body(_ state: MockRoomSummaryProviderState) -> some View {
|
||||||
|
|||||||
@@ -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 SwiftUI
|
||||||
|
|
||||||
|
struct HomeScreenInvitesButton: View {
|
||||||
|
@ScaledMetric private var badgeSize = 12.0
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
let hasBadge: Bool
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(title)
|
||||||
|
.foregroundColor(.element.primaryContent)
|
||||||
|
.font(.compound.bodyMD)
|
||||||
|
|
||||||
|
if hasBadge {
|
||||||
|
badge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var badge: some View {
|
||||||
|
Circle()
|
||||||
|
.frame(width: badgeSize, height: badgeSize)
|
||||||
|
.foregroundColor(.element.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvitesButton_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
HomeScreenInvitesButton(title: "Invites", hasBadge: true, action: { })
|
||||||
|
.previewDisplayName("Badge on")
|
||||||
|
|
||||||
|
HomeScreenInvitesButton(title: "Invites", hasBadge: false, action: { })
|
||||||
|
.previewDisplayName("Badge off")
|
||||||
|
|
||||||
|
HomeScreenInvitesButton(title: "Invites", hasBadge: true, action: { })
|
||||||
|
.previewDisplayName("Badge on (AX1)")
|
||||||
|
.dynamicTypeSize(.accessibility1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,7 +165,7 @@ private extension View {
|
|||||||
|
|
||||||
struct HomeScreenRoomCell_Previews: PreviewProvider {
|
struct HomeScreenRoomCell_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded)
|
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockRooms))
|
||||||
|
|
||||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider),
|
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider),
|
||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct InvitesCoordinatorParameters {
|
||||||
|
let userSession: UserSessionProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InvitesCoordinatorAction { }
|
||||||
|
|
||||||
|
final class InvitesCoordinator: CoordinatorProtocol {
|
||||||
|
private let parameters: InvitesCoordinatorParameters
|
||||||
|
private var viewModel: InvitesViewModelProtocol
|
||||||
|
private let actionsSubject: PassthroughSubject<InvitesCoordinatorAction, Never> = .init()
|
||||||
|
private var cancellables: Set<AnyCancellable> = .init()
|
||||||
|
|
||||||
|
var actions: AnyPublisher<InvitesCoordinatorAction, Never> {
|
||||||
|
actionsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(parameters: InvitesCoordinatorParameters) {
|
||||||
|
self.parameters = parameters
|
||||||
|
viewModel = InvitesViewModel(userSession: parameters.userSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
viewModel.actions.sink { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPresentable() -> AnyView {
|
||||||
|
AnyView(InvitesScreen(context: viewModel.context))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
enum InvitesViewModelAction { }
|
||||||
|
|
||||||
|
struct InvitesViewState: BindableState {
|
||||||
|
var invites: [InvitesRoomDetails]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvitesRoomDetails {
|
||||||
|
let roomDetails: RoomSummaryDetails
|
||||||
|
var inviter: RoomMemberProxyProtocol?
|
||||||
|
|
||||||
|
var isDirect: Bool {
|
||||||
|
roomDetails.isDirect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InvitesViewAction {
|
||||||
|
case accept(InvitesRoomDetails)
|
||||||
|
case decline(InvitesRoomDetails)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
typealias InvitesViewModelType = StateStoreViewModel<InvitesViewState, InvitesViewAction>
|
||||||
|
|
||||||
|
class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
|
||||||
|
private var actionsSubject: PassthroughSubject<InvitesViewModelAction, Never> = .init()
|
||||||
|
private let userSession: UserSessionProtocol
|
||||||
|
|
||||||
|
var actions: AnyPublisher<InvitesViewModelAction, Never> {
|
||||||
|
actionsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(userSession: UserSessionProtocol) {
|
||||||
|
self.userSession = userSession
|
||||||
|
super.init(initialViewState: InvitesViewState(), imageProvider: userSession.mediaProvider)
|
||||||
|
setupSubscriptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
override func process(viewAction: InvitesViewAction) {
|
||||||
|
switch viewAction {
|
||||||
|
case .accept:
|
||||||
|
break
|
||||||
|
case .decline:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var invitesSummaryProvider: RoomSummaryProviderProtocol? {
|
||||||
|
userSession.clientProxy.invitesSummaryProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupSubscriptions() {
|
||||||
|
guard let invitesSummaryProvider else {
|
||||||
|
MXLog.error("Room summary provider unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invitesSummaryProvider.roomListPublisher
|
||||||
|
.sink { [weak self] roomSummaries in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
let invites = roomSummaries.invites
|
||||||
|
self.state.invites = invites
|
||||||
|
|
||||||
|
for invite in invites {
|
||||||
|
self.fetchInviter(for: invite.roomDetails.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchInviter(for roomID: String) {
|
||||||
|
Task {
|
||||||
|
guard let room: RoomProxyProtocol = await self.userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let inviter: RoomMemberProxyProtocol? = await room.inviter()
|
||||||
|
|
||||||
|
guard let inviter, let inviteIndex = state.invites?.firstIndex(where: { $0.roomDetails.id == roomID }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.invites?[inviteIndex].inviter = inviter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Array where Element == RoomSummary {
|
||||||
|
var invites: [InvitesRoomDetails] {
|
||||||
|
compactMap { summary in
|
||||||
|
guard case .filled(let details) = summary else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .init(roomDetails: details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol InvitesViewModelProtocol {
|
||||||
|
var actions: AnyPublisher<InvitesViewModelAction, Never> { get }
|
||||||
|
var context: InvitesViewModelType.Context { get }
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct InvitesScreen: View {
|
||||||
|
@ObservedObject var context: InvitesViewModel.Context
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
if let rooms = context.viewState.invites, !rooms.isEmpty {
|
||||||
|
LazyVStack {
|
||||||
|
ForEach(rooms, id: \.roomDetails.id) { invite in
|
||||||
|
InvitesScreenCell(invite: invite,
|
||||||
|
imageProvider: context.imageProvider,
|
||||||
|
acceptAction: { context.send(viewAction: .accept(invite)) },
|
||||||
|
declineAction: { context.send(viewAction: .decline(invite)) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
noInvitesContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.actionInvitesList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var noInvitesContent: some View {
|
||||||
|
Text(L10n.screenInvitesEmptyList)
|
||||||
|
.font(.element.body)
|
||||||
|
.foregroundColor(.element.tertiaryContent)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.invitesScreen.noInvites)
|
||||||
|
.padding(.top, 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct InvitesScreen_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NavigationView {
|
||||||
|
InvitesScreen(context: InvitesViewModel.noInvites.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("No Invites")
|
||||||
|
|
||||||
|
NavigationView {
|
||||||
|
InvitesScreen(context: InvitesViewModel.someInvite.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Some Invite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension InvitesViewModel {
|
||||||
|
static let noInvites: InvitesViewModel = {
|
||||||
|
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||||
|
mediaProvider: MockMediaProvider())
|
||||||
|
let regularViewModel = InvitesViewModel(userSession: userSession)
|
||||||
|
return regularViewModel
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let someInvite: InvitesViewModel = {
|
||||||
|
let clientProxy = MockClientProxy(userID: "@userid:example.com")
|
||||||
|
clientProxy.invitesSummaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||||
|
clientProxy.visibleRoomsSummaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||||
|
let userSession = MockUserSession(clientProxy: clientProxy,
|
||||||
|
mediaProvider: MockMediaProvider())
|
||||||
|
let regularViewModel = InvitesViewModel(userSession: userSession)
|
||||||
|
return regularViewModel
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct InvitesScreenCell: View {
|
||||||
|
let invite: InvitesRoomDetails
|
||||||
|
let imageProvider: ImageProviderProtocol?
|
||||||
|
let acceptAction: () -> Void
|
||||||
|
let declineAction: () -> Void
|
||||||
|
|
||||||
|
private let verticalInsets = 16.0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 16) {
|
||||||
|
LoadableAvatarImage(url: mainAvatarURL,
|
||||||
|
name: title,
|
||||||
|
contentID: invite.roomDetails.id,
|
||||||
|
avatarSize: .custom(52),
|
||||||
|
imageProvider: imageProvider)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
|
mainContent
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.bottom, verticalInsets)
|
||||||
|
.overlay(alignment: .bottom) {
|
||||||
|
separator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, verticalInsets)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var mainContent: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.element.headline)
|
||||||
|
.foregroundColor(.element.primaryContent)
|
||||||
|
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.compound.bodyMD)
|
||||||
|
.foregroundColor(.compound.textPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
inviterView
|
||||||
|
|
||||||
|
buttons
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var inviterView: some View {
|
||||||
|
if let invitedText = attributedInviteText, let name = invite.inviter?.displayName {
|
||||||
|
HStack {
|
||||||
|
LoadableAvatarImage(url: invite.inviter?.avatarURL,
|
||||||
|
name: name,
|
||||||
|
contentID: name,
|
||||||
|
avatarSize: .custom(16),
|
||||||
|
imageProvider: imageProvider)
|
||||||
|
|
||||||
|
Text(invitedText)
|
||||||
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var buttons: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button(L10n.actionDecline, action: declineAction)
|
||||||
|
.buttonStyle(.elementCapsule)
|
||||||
|
|
||||||
|
Button(L10n.actionAccept, action: acceptAction)
|
||||||
|
.buttonStyle(.elementCapsuleProminent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var separator: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.element.quinaryContent)
|
||||||
|
.frame(height: 1 / UIScreen.main.scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
#warning("Return just `roomDetails.avatarURL` when this logic is implemented in the rust sdk")
|
||||||
|
private var mainAvatarURL: URL? {
|
||||||
|
invite.isDirect ? invite.inviter?.avatarURL : invite.roomDetails.avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
private var title: String {
|
||||||
|
invite.roomDetails.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subtitle: String? {
|
||||||
|
invite.isDirect ? invite.inviter?.userID : invite.roomDetails.canonicalAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
private var attributedInviteText: AttributedString? {
|
||||||
|
guard invite.roomDetails.isDirect == false, let inviterName = invite.inviter?.displayName else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = L10n.screenInvitesInvitedYou(inviterName)
|
||||||
|
var attributedString = AttributedString(text)
|
||||||
|
attributedString.font = .compound.bodyMD
|
||||||
|
attributedString.foregroundColor = .compound.textPlaceholder
|
||||||
|
if let range = attributedString.range(of: inviterName) {
|
||||||
|
attributedString[range].foregroundColor = .compound.textPrimary
|
||||||
|
}
|
||||||
|
return attributedString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvitesScreenCell_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
InvitesScreenCell(invite: .dm, imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||||
|
.previewDisplayName("Direct room")
|
||||||
|
|
||||||
|
InvitesScreenCell(invite: .room(alias: nil), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||||
|
.previewDisplayName("Default room")
|
||||||
|
|
||||||
|
InvitesScreenCell(invite: .room(alias: "#footest:somewhere.org"), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||||
|
.previewDisplayName("Aliased room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private extension InvitesRoomDetails {
|
||||||
|
static var dm: InvitesRoomDetails {
|
||||||
|
let dmRoom = RoomSummaryDetails(id: "@someone:somewhere.com",
|
||||||
|
name: "Some Guy",
|
||||||
|
isDirect: true,
|
||||||
|
avatarURL: nil,
|
||||||
|
lastMessage: nil,
|
||||||
|
lastMessageFormattedTimestamp: nil,
|
||||||
|
unreadNotificationCount: 0,
|
||||||
|
canonicalAlias: "#footest:somewhere.org")
|
||||||
|
let inviter = RoomMemberProxyMock()
|
||||||
|
inviter.displayName = "Jack"
|
||||||
|
inviter.userID = "@jack:somewhere.com"
|
||||||
|
|
||||||
|
return .init(roomDetails: dmRoom, inviter: inviter)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func room(alias: String?) -> InvitesRoomDetails {
|
||||||
|
let dmRoom = RoomSummaryDetails(id: "@someone:somewhere.com",
|
||||||
|
name: "Awesome Room",
|
||||||
|
isDirect: false,
|
||||||
|
avatarURL: nil,
|
||||||
|
lastMessage: nil,
|
||||||
|
lastMessageFormattedTimestamp: nil,
|
||||||
|
unreadNotificationCount: 0,
|
||||||
|
canonicalAlias: alias)
|
||||||
|
let inviter = RoomMemberProxyMock()
|
||||||
|
inviter.displayName = "Luca"
|
||||||
|
inviter.userID = "@jack:somewhere.com"
|
||||||
|
inviter.avatarURL = nil
|
||||||
|
|
||||||
|
return .init(roomDetails: dmRoom, inviter: inviter)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,10 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
var allRoomsViewProxy: SlidingSyncViewProxy?
|
var allRoomsViewProxy: SlidingSyncViewProxy?
|
||||||
var allRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
var allRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
|
|
||||||
|
var invitesSlidingSyncView: SlidingSyncList?
|
||||||
|
var invitesViewProxy: SlidingSyncViewProxy?
|
||||||
|
var invitesSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
|
|
||||||
private var loadCachedAvatarURLTask: Task<Void, Never>?
|
private var loadCachedAvatarURLTask: Task<Void, Never>?
|
||||||
private let avatarURLSubject = CurrentValueSubject<URL?, Never>(nil)
|
private let avatarURLSubject = CurrentValueSubject<URL?, Never>(nil)
|
||||||
var avatarURLPublisher: AnyPublisher<URL?, Never> {
|
var avatarURLPublisher: AnyPublisher<URL?, Never> {
|
||||||
@@ -303,6 +307,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
// cold cache state and count updates will be lost
|
// cold cache state and count updates will be lost
|
||||||
buildAndConfigureVisibleRoomsSlidingSyncView()
|
buildAndConfigureVisibleRoomsSlidingSyncView()
|
||||||
buildAndConfigureAllRoomsSlidingSyncView()
|
buildAndConfigureAllRoomsSlidingSyncView()
|
||||||
|
buildAndConfigureInvitesSlidingSyncView()
|
||||||
|
|
||||||
guard let visibleRoomsSlidingSyncView else {
|
guard let visibleRoomsSlidingSyncView else {
|
||||||
MXLog.error("Visible rooms sliding sync view unavailable")
|
MXLog.error("Visible rooms sliding sync view unavailable")
|
||||||
@@ -320,6 +325,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
// Don't forget to update the view proxies after building the slidingSync
|
// Don't forget to update the view proxies after building the slidingSync
|
||||||
visibleRoomsViewProxy?.setSlidingSync(slidingSync: slidingSync)
|
visibleRoomsViewProxy?.setSlidingSync(slidingSync: slidingSync)
|
||||||
allRoomsViewProxy?.setSlidingSync(slidingSync: slidingSync)
|
allRoomsViewProxy?.setSlidingSync(slidingSync: slidingSync)
|
||||||
|
invitesViewProxy?.setSlidingSync(slidingSync: slidingSync)
|
||||||
|
|
||||||
slidingSync.setObserver(observer: WeakClientProxyWrapper(clientProxy: self))
|
slidingSync.setObserver(observer: WeakClientProxyWrapper(clientProxy: self))
|
||||||
|
|
||||||
@@ -401,9 +407,41 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func buildAndConfigureInvitesSlidingSyncView() {
|
||||||
|
guard invitesSlidingSyncView == nil else {
|
||||||
|
fatalError("This shouldn't be called more than once")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let invitesView = try SlidingSyncListBuilder()
|
||||||
|
.noTimelineLimit()
|
||||||
|
.requiredState(requiredState: slidingSyncInvitesRequiredState)
|
||||||
|
.filters(filters: slidingSyncInviteFilters)
|
||||||
|
.name(name: "Invites")
|
||||||
|
.syncMode(mode: .growing)
|
||||||
|
.batchSize(batchSize: 100)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
let invitesViewProxy = SlidingSyncViewProxy(slidingSyncView: invitesView, name: "Invites")
|
||||||
|
|
||||||
|
invitesSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: invitesViewProxy,
|
||||||
|
eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)))
|
||||||
|
|
||||||
|
invitesSlidingSyncView = invitesView
|
||||||
|
self.invitesViewProxy = invitesViewProxy
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed building the invites sliding sync view with error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lazy var slidingSyncRequiredState = [RequiredState(key: "m.room.avatar", value: ""),
|
private lazy var slidingSyncRequiredState = [RequiredState(key: "m.room.avatar", value: ""),
|
||||||
RequiredState(key: "m.room.encryption", value: "")]
|
RequiredState(key: "m.room.encryption", value: "")]
|
||||||
|
|
||||||
|
private lazy var slidingSyncInvitesRequiredState = [RequiredState(key: "m.room.avatar", value: ""),
|
||||||
|
RequiredState(key: "m.room.encryption", value: ""),
|
||||||
|
RequiredState(key: "m.room.member", value: "$ME"),
|
||||||
|
RequiredState(key: "m.room.canonical_alias", value: "")]
|
||||||
|
|
||||||
private lazy var slidingSyncFilters = SlidingSyncRequestListFilters(isDm: nil,
|
private lazy var slidingSyncFilters = SlidingSyncRequestListFilters(isDm: nil,
|
||||||
spaces: [],
|
spaces: [],
|
||||||
isEncrypted: nil,
|
isEncrypted: nil,
|
||||||
@@ -415,6 +453,17 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
tags: [],
|
tags: [],
|
||||||
notTags: [])
|
notTags: [])
|
||||||
|
|
||||||
|
private lazy var slidingSyncInviteFilters = SlidingSyncRequestListFilters(isDm: nil,
|
||||||
|
spaces: [],
|
||||||
|
isEncrypted: nil,
|
||||||
|
isInvite: true,
|
||||||
|
isTombstoned: false,
|
||||||
|
roomTypes: [],
|
||||||
|
notRoomTypes: ["m.space"],
|
||||||
|
roomNameLike: nil,
|
||||||
|
tags: [],
|
||||||
|
notTags: [])
|
||||||
|
|
||||||
private func configureViewsPostInitialSync() {
|
private func configureViewsPostInitialSync() {
|
||||||
if let visibleRoomsSlidingSyncView {
|
if let visibleRoomsSlidingSyncView {
|
||||||
MXLog.info("Setting visible rooms view timeline limit to \(SlidingSyncConstants.lastMessageTimelineLimit)")
|
MXLog.info("Setting visible rooms view timeline limit to \(SlidingSyncConstants.lastMessageTimelineLimit)")
|
||||||
@@ -430,6 +479,13 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
MXLog.error("All rooms sliding sync view unavailable")
|
MXLog.error("All rooms sliding sync view unavailable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let invitesSlidingSyncView {
|
||||||
|
MXLog.info("Registering invites view")
|
||||||
|
_ = slidingSync?.addList(list: invitesSlidingSyncView)
|
||||||
|
} else {
|
||||||
|
MXLog.error("Invites sliding sync view unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
restartSync()
|
restartSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
|
|
||||||
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? { get }
|
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? { get }
|
||||||
|
|
||||||
|
var invitesSummaryProvider: RoomSummaryProviderProtocol? { get }
|
||||||
|
|
||||||
func startSync()
|
func startSync()
|
||||||
|
|
||||||
func stopSync()
|
func stopSync()
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
||||||
|
|
||||||
|
var invitesSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
||||||
|
|
||||||
var avatarURLPublisher: AnyPublisher<URL?, Never> { Empty().eraseToAnyPublisher() }
|
var avatarURLPublisher: AnyPublisher<URL?, Never> { Empty().eraseToAnyPublisher() }
|
||||||
|
|
||||||
internal init(userID: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) {
|
internal init(userID: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) {
|
||||||
@@ -51,6 +53,8 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
.failure(.failedCreatingRoom)
|
.failure(.failedCreatingRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var roomInviter: RoomMemberProxyMock?
|
||||||
|
@MainActor
|
||||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
||||||
guard let room = visibleRoomsSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else {
|
guard let room = visibleRoomsSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else {
|
||||||
return nil
|
return nil
|
||||||
@@ -58,9 +62,11 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
switch room {
|
switch room {
|
||||||
case .empty:
|
case .empty:
|
||||||
return await RoomProxyMock(with: .init(displayName: "Empty room"))
|
return RoomProxyMock(with: .init(displayName: "Empty room"))
|
||||||
case .filled(let details), .invalidated(let details):
|
case .filled(let details), .invalidated(let details):
|
||||||
return await RoomProxyMock(with: .init(displayName: details.name))
|
let room = RoomProxyMock(with: .init(displayName: details.name))
|
||||||
|
room.inviterReturnValue = roomInviter
|
||||||
|
return room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -338,6 +338,16 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inviter() async -> RoomMemberProxyProtocol? {
|
||||||
|
let inviter = await Task.dispatch(on: .global()) {
|
||||||
|
self.room.inviter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return inviter.map {
|
||||||
|
RoomMemberProxy(member: $0, backgroundTaskService: self.backgroundTaskService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ protocol RoomProxyProtocol {
|
|||||||
func leaveRoom() async -> Result<Void, RoomProxyError>
|
func leaveRoom() async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
func updateMembers() async
|
func updateMembers() async
|
||||||
|
|
||||||
|
func inviter() async -> RoomMemberProxyProtocol?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomProxyProtocol {
|
extension RoomProxyProtocol {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import Foundation
|
|||||||
|
|
||||||
enum MockRoomSummaryProviderState {
|
enum MockRoomSummaryProviderState {
|
||||||
case loading
|
case loading
|
||||||
case loaded
|
case loaded([RoomSummary])
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||||
@@ -37,38 +37,59 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
roomListPublisher = .init([])
|
roomListPublisher = .init([])
|
||||||
statePublisher = .init(.notLoaded)
|
statePublisher = .init(.notLoaded)
|
||||||
countPublisher = .init(0)
|
countPublisher = .init(0)
|
||||||
case .loaded:
|
case .loaded(let rooms):
|
||||||
roomListPublisher = .init(Self.rooms)
|
roomListPublisher = .init(rooms)
|
||||||
statePublisher = .init(.fullyLoaded)
|
statePublisher = .init(.fullyLoaded)
|
||||||
countPublisher = .init(UInt(Self.rooms.count))
|
countPublisher = .init(UInt(rooms.count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateVisibleRange(_ range: Range<Int>, timelineLimit: UInt) { }
|
func updateVisibleRange(_ range: Range<Int>, timelineLimit: UInt) { }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
extension Array where Element == RoomSummary {
|
||||||
|
static let mockRooms: [Element] = [
|
||||||
static let rooms: [RoomSummary] = [
|
|
||||||
.filled(details: RoomSummaryDetails(id: "1", name: "First room",
|
.filled(details: RoomSummaryDetails(id: "1", name: "First room",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
|
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
|
||||||
lastMessageFormattedTimestamp: "Now",
|
lastMessageFormattedTimestamp: "Now",
|
||||||
unreadNotificationCount: 4)),
|
unreadNotificationCount: 4,
|
||||||
|
canonicalAlias: nil)),
|
||||||
.filled(details: RoomSummaryDetails(id: "2",
|
.filled(details: RoomSummaryDetails(id: "2",
|
||||||
name: "Second room",
|
name: "Second room",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: URL.picturesDirectory,
|
avatarURL: URL.picturesDirectory,
|
||||||
lastMessage: nil,
|
lastMessage: nil,
|
||||||
lastMessageFormattedTimestamp: nil,
|
lastMessageFormattedTimestamp: nil,
|
||||||
unreadNotificationCount: 1)),
|
unreadNotificationCount: 1,
|
||||||
|
canonicalAlias: nil)),
|
||||||
.filled(details: RoomSummaryDetails(id: "3",
|
.filled(details: RoomSummaryDetails(id: "3",
|
||||||
name: "Third room",
|
name: "Third room",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
|
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
|
||||||
lastMessageFormattedTimestamp: "Later",
|
lastMessageFormattedTimestamp: "Later",
|
||||||
unreadNotificationCount: 0)),
|
unreadNotificationCount: 0,
|
||||||
|
canonicalAlias: nil)),
|
||||||
.empty
|
.empty
|
||||||
]
|
]
|
||||||
|
|
||||||
|
static let mockInvites: [Element] = [
|
||||||
|
.filled(details: RoomSummaryDetails(id: "someAwesomeRoomId1", name: "First room",
|
||||||
|
isDirect: false,
|
||||||
|
avatarURL: URL.picturesDirectory,
|
||||||
|
lastMessage: nil,
|
||||||
|
lastMessageFormattedTimestamp: nil,
|
||||||
|
unreadNotificationCount: 0,
|
||||||
|
canonicalAlias: "#footest:somewhere.org")),
|
||||||
|
.filled(details: RoomSummaryDetails(id: "someAwesomeRoomId2",
|
||||||
|
name: "Second room",
|
||||||
|
isDirect: true,
|
||||||
|
avatarURL: nil,
|
||||||
|
lastMessage: nil,
|
||||||
|
lastMessageFormattedTimestamp: nil,
|
||||||
|
unreadNotificationCount: 0,
|
||||||
|
canonicalAlias: nil))
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct RoomSummaryDetails {
|
|||||||
let lastMessage: AttributedString?
|
let lastMessage: AttributedString?
|
||||||
let lastMessageFormattedTimestamp: String?
|
let lastMessageFormattedTimestamp: String?
|
||||||
let unreadNotificationCount: UInt
|
let unreadNotificationCount: UInt
|
||||||
|
let canonicalAlias: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomSummaryDetails: CustomStringConvertible {
|
extension RoomSummaryDetails: CustomStringConvertible {
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarURL = room.fullRoom()?.avatarUrl().flatMap(URL.init(string:))
|
let fullRoom = room.fullRoom()
|
||||||
|
let avatarURL = fullRoom?.avatarUrl().flatMap(URL.init(string:))
|
||||||
|
let canonicalAlias = fullRoom?.canonicalAlias()
|
||||||
|
|
||||||
let details = RoomSummaryDetails(id: room.roomId(),
|
let details = RoomSummaryDetails(id: room.roomId(),
|
||||||
name: room.name() ?? room.roomId(),
|
name: room.name() ?? room.roomId(),
|
||||||
@@ -119,7 +121,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
avatarURL: avatarURL,
|
avatarURL: avatarURL,
|
||||||
lastMessage: attributedLastMessage,
|
lastMessage: attributedLastMessage,
|
||||||
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
|
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
|
||||||
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()))
|
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()),
|
||||||
|
canonicalAlias: canonicalAlias)
|
||||||
|
|
||||||
return invalidated ? .invalidated(details: details) : .filled(details: details)
|
return invalidated ? .invalidated(details: details) : .filled(details: details)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum UserSessionFlowCoordinatorAction {
|
enum UserSessionFlowCoordinatorAction {
|
||||||
@@ -22,6 +23,7 @@ enum UserSessionFlowCoordinatorAction {
|
|||||||
|
|
||||||
class UserSessionFlowCoordinator: CoordinatorProtocol {
|
class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||||
private let stateMachine: UserSessionFlowCoordinatorStateMachine
|
private let stateMachine: UserSessionFlowCoordinatorStateMachine
|
||||||
|
private var cancellables: Set<AnyCancellable> = .init()
|
||||||
|
|
||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let navigationSplitCoordinator: NavigationSplitCoordinator
|
private let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||||
@@ -64,7 +66,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
func handleAppRoute(_ appRoute: AppRoute) {
|
func handleAppRoute(_ appRoute: AppRoute) {
|
||||||
switch stateMachine.state {
|
switch stateMachine.state {
|
||||||
case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen:
|
case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen:
|
||||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||||
case .roomList, .initial:
|
case .roomList, .initial:
|
||||||
break
|
break
|
||||||
@@ -115,6 +117,10 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
self.presentStartChat()
|
self.presentStartChat()
|
||||||
case (.startChatScreen, .dismissedStartChatScreen, .roomList):
|
case (.startChatScreen, .dismissedStartChatScreen, .roomList):
|
||||||
break
|
break
|
||||||
|
case (.roomList, .showInvitesScreen, .invitesScreen):
|
||||||
|
self.presentInvitesList()
|
||||||
|
case (.invitesScreen, .closedInvitesScreen, .roomList):
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
fatalError("Unknown transition: \(context)")
|
fatalError("Unknown transition: \(context)")
|
||||||
}
|
}
|
||||||
@@ -150,6 +156,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
self.stateMachine.processEvent(.showStartChatScreen)
|
self.stateMachine.processEvent(.showStartChatScreen)
|
||||||
case .signOut:
|
case .signOut:
|
||||||
self.callback?(.signOut)
|
self.callback?(.signOut)
|
||||||
|
case .presentInvitesScreen:
|
||||||
|
self.stateMachine.processEvent(.showInvitesScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,4 +321,19 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
self?.stateMachine.processEvent(.dismissedFeedbackScreen)
|
self?.stateMachine.processEvent(.dismissedFeedbackScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Invites list
|
||||||
|
|
||||||
|
private func presentInvitesList() {
|
||||||
|
let parameters = InvitesCoordinatorParameters(userSession: userSession)
|
||||||
|
let coordinator = InvitesCoordinator(parameters: parameters)
|
||||||
|
|
||||||
|
coordinator.actions
|
||||||
|
.sink { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setDetailCoordinator(coordinator) { [weak self] in
|
||||||
|
self?.stateMachine.processEvent(.closedInvitesScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
|
|
||||||
/// Showing the start chat screen
|
/// Showing the start chat screen
|
||||||
case startChatScreen(selectedRoomId: String?)
|
case startChatScreen(selectedRoomId: String?)
|
||||||
|
|
||||||
|
/// Showing invites list screen
|
||||||
|
case invitesScreen(selectedRoomId: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Events that can be triggered on the AppCoordinator state machine
|
/// Events that can be triggered on the AppCoordinator state machine
|
||||||
@@ -69,6 +72,11 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
case showStartChatScreen
|
case showStartChatScreen
|
||||||
/// Start chat has been dismissed
|
/// Start chat has been dismissed
|
||||||
case dismissedStartChatScreen
|
case dismissedStartChatScreen
|
||||||
|
|
||||||
|
/// Request presentation of the invites screen
|
||||||
|
case showInvitesScreen
|
||||||
|
/// The invites screen has been dismissed
|
||||||
|
case closedInvitesScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
private let stateMachine: StateMachine<State, Event>
|
private let stateMachine: StateMachine<State, Event>
|
||||||
@@ -112,6 +120,12 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
return .startChatScreen(selectedRoomId: selectedRoomId)
|
return .startChatScreen(selectedRoomId: selectedRoomId)
|
||||||
case (.dismissedStartChatScreen, .startChatScreen(let selectedRoomId)):
|
case (.dismissedStartChatScreen, .startChatScreen(let selectedRoomId)):
|
||||||
return .roomList(selectedRoomId: selectedRoomId)
|
return .roomList(selectedRoomId: selectedRoomId)
|
||||||
|
|
||||||
|
case (.showInvitesScreen, .roomList(let selectedRoomId)):
|
||||||
|
return .invitesScreen(selectedRoomId: selectedRoomId)
|
||||||
|
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
|
||||||
|
return .roomList(selectedRoomId: selectedRoomId)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ class MockScreen: Identifiable {
|
|||||||
case .userSessionScreen:
|
case .userSessionScreen:
|
||||||
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
||||||
|
|
||||||
let clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded))
|
let clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms)))
|
||||||
|
|
||||||
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
|
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
|
||||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||||
@@ -359,6 +359,23 @@ class MockScreen: Identifiable {
|
|||||||
mediaProvider: MockMediaProvider()))
|
mediaProvider: MockMediaProvider()))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
|
case .invites:
|
||||||
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
|
let clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||||
|
clientProxy.roomInviter = RoomMemberProxyMock.mockCharlie
|
||||||
|
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||||
|
clientProxy.visibleRoomsSummaryProvider = summaryProvider
|
||||||
|
clientProxy.invitesSummaryProvider = summaryProvider
|
||||||
|
|
||||||
|
let coordinator = InvitesCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
return navigationStackCoordinator
|
||||||
|
case .invitesNoInvites:
|
||||||
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
|
let clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||||
|
let coordinator = InvitesCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
return navigationStackCoordinator
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case startChat
|
case startChat
|
||||||
case startChatWithSearchResults
|
case startChatWithSearchResults
|
||||||
case startChatSearchingNonexistentID
|
case startChatSearchingNonexistentID
|
||||||
|
case invites
|
||||||
|
case invitesNoInvites
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UITestsScreenIdentifier: CustomStringConvertible {
|
extension UITestsScreenIdentifier: CustomStringConvertible {
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ class TemplateScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var viewModel: TemplateViewModelProtocol!
|
var viewModel: TemplateViewModelProtocol!
|
||||||
var context: TemplateViewModelType.Context!
|
|
||||||
|
|
||||||
@MainActor override func setUpWithError() throws {
|
var context: TemplateViewModelType.Context {
|
||||||
|
viewModel.context
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
viewModel = TemplateViewModel(promptType: .regular, initialCount: Constants.counterInitialValue)
|
viewModel = TemplateViewModel(promptType: .regular, initialCount: Constants.counterInitialValue)
|
||||||
context = viewModel.context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInitialState() {
|
func testInitialState() {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// 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 ElementX
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class InvitesScreenUITests: XCTestCase {
|
||||||
|
func testMixedInvites() {
|
||||||
|
let app = Application.launch(.invites)
|
||||||
|
app.assertScreenshot(.invites)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNoInvites() {
|
||||||
|
let app = Application.launch(.invitesNoInvites)
|
||||||
|
XCTAssertTrue(app.staticTexts[A11yIdentifiers.invitesScreen.noInvites].exists)
|
||||||
|
app.assertScreenshot(.invitesNoInvites)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8b7e4017ecafaca01e672701182dd6695573b021bc684c285e76214f311b35f6
|
||||||
|
size 98707
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2f31e501d332a6a5000233961639d85327e8bf767e4d47a7c78433dc5bb8803b
|
||||||
|
size 59517
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:947fbd514af15087b850aae85371920b9f829fe92c0bbb4ca62ac97e95b6a92d
|
||||||
|
size 116523
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cdf43c35ecaafb6c0a167e3278303e52f9d497e95f6cecc396893250f83f2434
|
||||||
|
size 58645
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5e4ce0956d2c9499c1d4f6e84041377263ed74a959196e4f60574c8a7f694855
|
||||||
|
size 104489
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:949b61999f872d1b84ebf6f11e8988ec5d034c9e915f719daac76753363f6ad7
|
||||||
|
size 60966
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:dd3942891d03d9e67861fefd90406d51bb3189782307fc0335a1fba1c4754c0b
|
||||||
|
size 121889
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:356aa560b74edad92cc3437da4f3557fbda4fb697a0a025e23c095589c13ece9
|
||||||
|
size 60635
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class InvitesViewModelTests: XCTestCase {
|
||||||
|
var viewModel: InvitesViewModelProtocol!
|
||||||
|
var clientProxy: MockClientProxy!
|
||||||
|
var userSession: MockUserSession!
|
||||||
|
|
||||||
|
var context: InvitesViewModelType.Context {
|
||||||
|
viewModel.context
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
clientProxy = MockClientProxy(userID: "@a:b.com")
|
||||||
|
userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyState() throws {
|
||||||
|
setupViewModel()
|
||||||
|
let invites = try XCTUnwrap(context.viewState.invites)
|
||||||
|
XCTAssertTrue(invites.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testListState() throws {
|
||||||
|
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||||
|
clientProxy.invitesSummaryProvider = summaryProvider
|
||||||
|
clientProxy.visibleRoomsSummaryProvider = summaryProvider
|
||||||
|
setupViewModel()
|
||||||
|
let invites = try XCTUnwrap(context.viewState.invites)
|
||||||
|
XCTAssertEqual(invites.count, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func setupViewModel() {
|
||||||
|
viewModel = InvitesViewModel(userSession: userSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,7 +184,8 @@ class LoggingTests: XCTestCase {
|
|||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
lastMessage: AttributedString(lastMessage),
|
lastMessage: AttributedString(lastMessage),
|
||||||
lastMessageFormattedTimestamp: "Now",
|
lastMessageFormattedTimestamp: "Now",
|
||||||
unreadNotificationCount: 0)
|
unreadNotificationCount: 0,
|
||||||
|
canonicalAlias: nil)
|
||||||
|
|
||||||
// When logging that value
|
// When logging that value
|
||||||
XCTAssert(MXLogger.logFiles.isEmpty)
|
XCTAssert(MXLogger.logFiles.isEmpty)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Add invites list (UI only)
|
||||||
Reference in New Issue
Block a user