diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 4d4c5add3..271b341de 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -8195,7 +8195,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.73; + version = 1.0.74; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a40510449..0290c04df 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "b3f97292695e8d63469c0d3ec608eb74423c6a2e", - "version" : "1.0.73" + "revision" : "66d32e79ae20dd31201cd16eced53cfcc0c3239d", + "version" : "1.0.74" } }, { diff --git a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift index 65c71d8ea..79b48100c 100644 --- a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift @@ -59,7 +59,8 @@ extension RoomInfo { numUnreadMessages: 0, numUnreadNotifications: 0, numUnreadMentions: 0, - pinnedEventIds: []) + pinnedEventIds: [], + joinRule: .invite) } } diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 0cd26ef42..d8acfaf79 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -39,6 +39,7 @@ struct JoinedRoomProxyMockConfiguration { var canUserPin = true var shouldUseAutoUpdatingTimeline = false + var joinRule: JoinRule? } extension JoinedRoomProxyMock { @@ -166,6 +167,7 @@ extension RoomInfo { numUnreadMessages: 0, numUnreadNotifications: 0, numUnreadMentions: 0, - pinnedEventIds: Array(configuration.pinnedEventIDs)) + pinnedEventIds: Array(configuration.pinnedEventIDs), + joinRule: configuration.joinRule) } } diff --git a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift index 76559bae1..82d84d31a 100644 --- a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift @@ -57,6 +57,7 @@ extension RoomInfo { numUnreadMessages: 0, numUnreadNotifications: 0, numUnreadMentions: 0, - pinnedEventIds: []) + pinnedEventIds: [], + joinRule: .knock) } } diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift index 645914828..f45b0e6ec 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift @@ -11,9 +11,17 @@ enum KnockRequestsListScreenViewModelAction { } struct KnockRequestsListScreenViewState: BindableState { var requests: [KnockRequestCellInfo] = [] - var canAccept = false - var canDecline = false - var canBan = false + // If you are in this view one of these must have been true so by default we assume all of them to be true + var canAccept = true + var canDecline = true + var canBan = true + var isKnockableRoom = true + + // If all the permissions are denied or the join rule changes while we are in the view + // we want to stop displaying any request + var shouldDisplayRequests: Bool { + !requests.isEmpty && isKnockableRoom && (canAccept || canDecline || canBan) + } } enum KnockRequestsListScreenViewAction { diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift index 359319694..2c8327e22 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift @@ -22,6 +22,7 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn self.roomProxy = roomProxy super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider) + updateRoomInfo(roomInfo: roomProxy.infoPublisher.value) Task { await updatePermissions() } @@ -48,13 +49,23 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn private func setupSubscriptions() { roomProxy.infoPublisher - .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] _ in + .receive(on: DispatchQueue.main) + .sink { [weak self] roomInfo in + self?.updateRoomInfo(roomInfo: roomInfo) Task { await self?.updatePermissions() } } .store(in: &cancellables) } + private func updateRoomInfo(roomInfo: RoomInfoProxy) { + switch roomInfo.joinRule { + case .knock, .knockRestricted: + state.isKnockableRoom = true + default: + state.isKnockableRoom = false + } + } + private func updatePermissions() async { state.canAccept = await (try? roomProxy.canUserInvite(userID: roomProxy.ownUserID).get()) == true state.canDecline = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift index 792dba762..da8521674 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift @@ -17,12 +17,12 @@ struct KnockRequestsListScreen: View { .navigationTitle(L10n.screenKnockRequestsListTitle) .background(.compound.bgCanvasDefault) .overlay { - if context.viewState.requests.isEmpty { + if !context.viewState.shouldDisplayRequests { KnockRequestsListEmptyStateView() } } .safeAreaInset(edge: .bottom) { - if !context.viewState.requests.isEmpty { + if context.viewState.shouldDisplayRequests { acceptAllButton } } @@ -32,14 +32,16 @@ struct KnockRequestsListScreen: View { private var mainContent: some View { ScrollView { LazyVStack(spacing: 0) { - ForEach(context.viewState.requests) { requestInfo in - ListRow(kind: .custom { - KnockRequestCell(cellInfo: requestInfo, - mediaProvider: context.mediaProvider, - onAccept: context.viewState.canAccept ? onAccept : nil, - onDecline: context.viewState.canDecline ? onDecline : nil, - onDeclineAndBan: context.viewState.canBan ? onDeclineAndBan : nil) - }) + if context.viewState.shouldDisplayRequests { + ForEach(context.viewState.requests) { requestInfo in + ListRow(kind: .custom { + KnockRequestCell(cellInfo: requestInfo, + mediaProvider: context.mediaProvider, + onAccept: context.viewState.canAccept ? onAccept : nil, + onDecline: context.viewState.canDecline ? onDecline : nil, + onDeclineAndBan: context.viewState.canBan ? onDeclineAndBan : nil) + }) + } } } .padding(.top, 40) @@ -79,10 +81,7 @@ struct KnockRequestsListScreen_Previews: PreviewProvider, TestablePreview { // swiftlint:disable:next line_length .init(id: "@bob:matrix.org", displayName: "Bob", avatarUrl: nil, timestamp: "Now", reason: "Hello this one is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason"), .init(id: "@charlie:matrix.org", displayName: "Charlie", avatarUrl: nil, timestamp: "Now", reason: nil), - .init(id: "@dan:matrix.org", displayName: "Dan", avatarUrl: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")], - canAccept: true, - canDecline: true, - canBan: true)) + .init(id: "@dan:matrix.org", displayName: "Dan", avatarUrl: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")])) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index b91888417..cfd247cdc 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -49,9 +49,10 @@ struct RoomDetailsScreenViewState: BindableState { var canJoinCall = false var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading var knockingEnabled = false + var isKnockableRoom = false var canSeeKnockingRequests: Bool { - knockingEnabled && dmRecipient == nil && (canInviteUsers || canKickUsers || canBanUsers) + knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers) } var canEdit: Bool { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index ed7b4cf44..34a8d8a27 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -189,6 +189,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces() state.joinedMembersCount = roomInfo.joinedMembersCount state.bindings.isFavourite = roomInfo.isFavourite + switch roomInfo.joinRule { + case .knock, .knockRestricted: + state.isKnockableRoom = true + default: + state.isKnockableRoom = false + } } private func fetchMembersIfNeeded() async { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 1873fbf15..042ba4e61 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -323,7 +323,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { isDirect: false, isEncrypted: true, canonicalAlias: "#alias:domain.com", - members: members)) + members: members, + joinRule: .knock)) var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration() notificationSettingsProxyMockConfiguration.roomMode.isDefault = false @@ -378,7 +379,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { name: "Room A", isDirect: false, isEncrypted: false, - members: members)) + members: members, + joinRule: .knock)) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) return RoomDetailsScreenViewModel(roomProxy: roomProxy, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 92c9bfdbe..4c8ff2009 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -39,6 +39,18 @@ struct RoomScreenViewState: BindableState { var hasOngoingCall: Bool var shouldShowCallButton = true + var isKnockingEnabled = false + var isKnockableRoom = false + var canAcceptKnocks = false + var canDeclineKnocks = false + var canBan = false + // TODO: We still don't know how to get these, but these will be the non already seen knock requests of the room, for now we are using this as a mock for testing purposes + var unseenKnockRequests: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Helloooo")] + + var shouldSeeKnockRequests: Bool { + isKnockingEnabled && isKnockableRoom && !unseenKnockRequests.isEmpty && (canAcceptKnocks || canDeclineKnocks || canBan) + } + var footerDetails: RoomScreenFooterViewDetails? var bindings: RoomScreenViewStateBindings diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 93c95e1a3..3e43a4837 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -117,6 +117,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Private private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher) { + appSettings.$knockingEnabled + .weakAssign(to: \.state.isKnockingEnabled, on: self) + .store(in: &cancellables) + let roomInfoSubscription = roomProxy .infoPublisher @@ -236,10 +240,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) } - let userID = roomProxy.ownUserID - if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { - state.canJoinCall = permission + switch (roomProxy.isEncryptedOneToOneRoom, roomInfo.joinRule) { + case (false, .knock), (false, .knockRestricted): + state.isKnockableRoom = true + default: + state.isKnockableRoom = false } + + let ownUserID = roomProxy.ownUserID + state.canJoinCall = await (try? roomProxy.canUserJoinCall(userID: ownUserID).get()) == true + state.canAcceptKnocks = await (try? roomProxy.canUserInvite(userID: ownUserID).get()) == true + state.canDeclineKnocks = await (try? roomProxy.canUserKick(userID: ownUserID).get()) == true + state.canBan = await (try? roomProxy.canUserBan(userID: ownUserID).get()) == true } private func setupPinnedEventsTimelineProviderIfNeeded() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 9e187d8ab..3df34b643 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -29,12 +29,11 @@ struct RoomScreen: View { timeline .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .overlay(alignment: .top) { - Group { - if roomContext.viewState.shouldShowPinnedEventsBanner { - pinnedItemsBanner - } - } - .animation(.elementDefault, value: roomContext.viewState.shouldShowPinnedEventsBanner) + pinnedItemsBanner + } + // This can overlay on top of the pinnedItemsBanner + .overlay(alignment: .top) { + knockRequestsBanner } .safeAreaInset(edge: .bottom, spacing: 0) { VStack(spacing: 0) { @@ -119,11 +118,45 @@ struct RoomScreen: View { } } + @ViewBuilder private var pinnedItemsBanner: some View { - PinnedItemsBannerView(state: roomContext.viewState.pinnedEventsBannerState, - onMainButtonTap: { roomContext.send(viewAction: .tappedPinnedEventsBanner) }, - onViewAllButtonTap: { roomContext.send(viewAction: .viewAllPins) }) - .transition(.move(edge: .top)) + Group { + if roomContext.viewState.shouldShowPinnedEventsBanner { + PinnedItemsBannerView(state: roomContext.viewState.pinnedEventsBannerState, + onMainButtonTap: { roomContext.send(viewAction: .tappedPinnedEventsBanner) }, + onViewAllButtonTap: { roomContext.send(viewAction: .viewAllPins) }) + .transition(.move(edge: .top)) + } + } + .animation(.elementDefault, value: roomContext.viewState.shouldShowPinnedEventsBanner) + } + + @ViewBuilder + private var knockRequestsBanner: some View { + Group { + if roomContext.viewState.shouldSeeKnockRequests { + KnockRequestsBannerView(requests: roomContext.viewState.unseenKnockRequests, + onDismiss: dismissKnockRequestsBanner, + onAccept: roomContext.viewState.canAcceptKnocks ? acceptKnockRequest : nil, + onViewAll: onViewAllKnockRequests, + mediaProvider: roomContext.mediaProvider) + .padding(.top, 16) + .transition(.move(edge: .top)) + } + } + .animation(.elementDefault, value: roomContext.viewState.shouldSeeKnockRequests) + } + + private func dismissKnockRequestsBanner() { + // TODO: Implement + } + + private func acceptKnockRequest(userID: String) { + // TODO: Implement + } + + private func onViewAllKnockRequests() { + // TODO: Implement } private var scrollToBottomButton: some View { diff --git a/ElementX/Sources/Services/Room/RoomInfoProxy.swift b/ElementX/Sources/Services/Room/RoomInfoProxy.swift index 9cd290228..5e9da29e3 100644 --- a/ElementX/Sources/Services/Room/RoomInfoProxy.swift +++ b/ElementX/Sources/Services/Room/RoomInfoProxy.swift @@ -65,6 +65,7 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol { var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) } var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) } var pinnedEventIDs: Set { Set(roomInfo.pinnedEventIds) } + var joinRule: JoinRule? { roomInfo.joinRule } } struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { diff --git a/project.yml b/project.yml index 16c5b4624..9739ec753 100644 --- a/project.yml +++ b/project.yml @@ -61,7 +61,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.73 + exactVersion: 1.0.74 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios