Knock Requests banner display logic (#3550)

* added the join rule to the info

* update SDK and added logic to display the banner

in the room screen

* added the logic to display the banner
This commit is contained in:
Mauro
2024-11-25 13:14:02 +01:00
committed by GitHub
parent 9180bac13c
commit 26e07f2457
16 changed files with 131 additions and 42 deletions
+1 -1
View File
@@ -8195,7 +8195,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = 1.0.73; version = 1.0.74;
}; };
}; };
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
@@ -149,8 +149,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "b3f97292695e8d63469c0d3ec608eb74423c6a2e", "revision" : "66d32e79ae20dd31201cd16eced53cfcc0c3239d",
"version" : "1.0.73" "version" : "1.0.74"
} }
}, },
{ {
@@ -59,7 +59,8 @@ extension RoomInfo {
numUnreadMessages: 0, numUnreadMessages: 0,
numUnreadNotifications: 0, numUnreadNotifications: 0,
numUnreadMentions: 0, numUnreadMentions: 0,
pinnedEventIds: []) pinnedEventIds: [],
joinRule: .invite)
} }
} }
@@ -39,6 +39,7 @@ struct JoinedRoomProxyMockConfiguration {
var canUserPin = true var canUserPin = true
var shouldUseAutoUpdatingTimeline = false var shouldUseAutoUpdatingTimeline = false
var joinRule: JoinRule?
} }
extension JoinedRoomProxyMock { extension JoinedRoomProxyMock {
@@ -166,6 +167,7 @@ extension RoomInfo {
numUnreadMessages: 0, numUnreadMessages: 0,
numUnreadNotifications: 0, numUnreadNotifications: 0,
numUnreadMentions: 0, numUnreadMentions: 0,
pinnedEventIds: Array(configuration.pinnedEventIDs)) pinnedEventIds: Array(configuration.pinnedEventIDs),
joinRule: configuration.joinRule)
} }
} }
@@ -57,6 +57,7 @@ extension RoomInfo {
numUnreadMessages: 0, numUnreadMessages: 0,
numUnreadNotifications: 0, numUnreadNotifications: 0,
numUnreadMentions: 0, numUnreadMentions: 0,
pinnedEventIds: []) pinnedEventIds: [],
joinRule: .knock)
} }
} }
@@ -11,9 +11,17 @@ enum KnockRequestsListScreenViewModelAction { }
struct KnockRequestsListScreenViewState: BindableState { struct KnockRequestsListScreenViewState: BindableState {
var requests: [KnockRequestCellInfo] = [] var requests: [KnockRequestCellInfo] = []
var canAccept = 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 canDecline = false var canAccept = true
var canBan = false 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 { enum KnockRequestsListScreenViewAction {
@@ -22,6 +22,7 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
self.roomProxy = roomProxy self.roomProxy = roomProxy
super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider) super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider)
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
Task { Task {
await updatePermissions() await updatePermissions()
} }
@@ -48,13 +49,23 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
private func setupSubscriptions() { private func setupSubscriptions() {
roomProxy.infoPublisher roomProxy.infoPublisher
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo: roomInfo)
Task { await self?.updatePermissions() } Task { await self?.updatePermissions() }
} }
.store(in: &cancellables) .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 { private func updatePermissions() async {
state.canAccept = await (try? roomProxy.canUserInvite(userID: roomProxy.ownUserID).get()) == true state.canAccept = await (try? roomProxy.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canDecline = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true state.canDecline = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true
@@ -17,12 +17,12 @@ struct KnockRequestsListScreen: View {
.navigationTitle(L10n.screenKnockRequestsListTitle) .navigationTitle(L10n.screenKnockRequestsListTitle)
.background(.compound.bgCanvasDefault) .background(.compound.bgCanvasDefault)
.overlay { .overlay {
if context.viewState.requests.isEmpty { if !context.viewState.shouldDisplayRequests {
KnockRequestsListEmptyStateView() KnockRequestsListEmptyStateView()
} }
} }
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
if !context.viewState.requests.isEmpty { if context.viewState.shouldDisplayRequests {
acceptAllButton acceptAllButton
} }
} }
@@ -32,14 +32,16 @@ struct KnockRequestsListScreen: View {
private var mainContent: some View { private var mainContent: some View {
ScrollView { ScrollView {
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
ForEach(context.viewState.requests) { requestInfo in if context.viewState.shouldDisplayRequests {
ListRow(kind: .custom { ForEach(context.viewState.requests) { requestInfo in
KnockRequestCell(cellInfo: requestInfo, ListRow(kind: .custom {
mediaProvider: context.mediaProvider, KnockRequestCell(cellInfo: requestInfo,
onAccept: context.viewState.canAccept ? onAccept : nil, mediaProvider: context.mediaProvider,
onDecline: context.viewState.canDecline ? onDecline : nil, onAccept: context.viewState.canAccept ? onAccept : nil,
onDeclineAndBan: context.viewState.canBan ? onDeclineAndBan : nil) onDecline: context.viewState.canDecline ? onDecline : nil,
}) onDeclineAndBan: context.viewState.canBan ? onDeclineAndBan : nil)
})
}
} }
} }
.padding(.top, 40) .padding(.top, 40)
@@ -79,10 +81,7 @@ struct KnockRequestsListScreen_Previews: PreviewProvider, TestablePreview {
// swiftlint:disable:next line_length // 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: "@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: "@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!")], .init(id: "@dan:matrix.org", displayName: "Dan", avatarUrl: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")]))
canAccept: true,
canDecline: true,
canBan: true))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
@@ -49,9 +49,10 @@ struct RoomDetailsScreenViewState: BindableState {
var canJoinCall = false var canJoinCall = false
var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading
var knockingEnabled = false var knockingEnabled = false
var isKnockableRoom = false
var canSeeKnockingRequests: Bool { var canSeeKnockingRequests: Bool {
knockingEnabled && dmRecipient == nil && (canInviteUsers || canKickUsers || canBanUsers) knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers)
} }
var canEdit: Bool { var canEdit: Bool {
@@ -189,6 +189,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces() state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
state.joinedMembersCount = roomInfo.joinedMembersCount state.joinedMembersCount = roomInfo.joinedMembersCount
state.bindings.isFavourite = roomInfo.isFavourite state.bindings.isFavourite = roomInfo.isFavourite
switch roomInfo.joinRule {
case .knock, .knockRestricted:
state.isKnockableRoom = true
default:
state.isKnockableRoom = false
}
} }
private func fetchMembersIfNeeded() async { private func fetchMembersIfNeeded() async {
@@ -323,7 +323,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
isDirect: false, isDirect: false,
isEncrypted: true, isEncrypted: true,
canonicalAlias: "#alias:domain.com", canonicalAlias: "#alias:domain.com",
members: members)) members: members,
joinRule: .knock))
var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration() var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration()
notificationSettingsProxyMockConfiguration.roomMode.isDefault = false notificationSettingsProxyMockConfiguration.roomMode.isDefault = false
@@ -378,7 +379,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
name: "Room A", name: "Room A",
isDirect: false, isDirect: false,
isEncrypted: false, isEncrypted: false,
members: members)) members: members,
joinRule: .knock))
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
return RoomDetailsScreenViewModel(roomProxy: roomProxy, return RoomDetailsScreenViewModel(roomProxy: roomProxy,
@@ -39,6 +39,18 @@ struct RoomScreenViewState: BindableState {
var hasOngoingCall: Bool var hasOngoingCall: Bool
var shouldShowCallButton = true 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 footerDetails: RoomScreenFooterViewDetails?
var bindings: RoomScreenViewStateBindings var bindings: RoomScreenViewStateBindings
@@ -117,6 +117,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Private // MARK: - Private
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) { private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
appSettings.$knockingEnabled
.weakAssign(to: \.state.isKnockingEnabled, on: self)
.store(in: &cancellables)
let roomInfoSubscription = roomProxy let roomInfoSubscription = roomProxy
.infoPublisher .infoPublisher
@@ -236,10 +240,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count)
} }
let userID = roomProxy.ownUserID switch (roomProxy.isEncryptedOneToOneRoom, roomInfo.joinRule) {
if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { case (false, .knock), (false, .knockRestricted):
state.canJoinCall = permission 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() { private func setupPinnedEventsTimelineProviderIfNeeded() {
@@ -29,12 +29,11 @@ struct RoomScreen: View {
timeline timeline
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.overlay(alignment: .top) { .overlay(alignment: .top) {
Group { pinnedItemsBanner
if roomContext.viewState.shouldShowPinnedEventsBanner { }
pinnedItemsBanner // This can overlay on top of the pinnedItemsBanner
} .overlay(alignment: .top) {
} knockRequestsBanner
.animation(.elementDefault, value: roomContext.viewState.shouldShowPinnedEventsBanner)
} }
.safeAreaInset(edge: .bottom, spacing: 0) { .safeAreaInset(edge: .bottom, spacing: 0) {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -119,11 +118,45 @@ struct RoomScreen: View {
} }
} }
@ViewBuilder
private var pinnedItemsBanner: some View { private var pinnedItemsBanner: some View {
PinnedItemsBannerView(state: roomContext.viewState.pinnedEventsBannerState, Group {
onMainButtonTap: { roomContext.send(viewAction: .tappedPinnedEventsBanner) }, if roomContext.viewState.shouldShowPinnedEventsBanner {
onViewAllButtonTap: { roomContext.send(viewAction: .viewAllPins) }) PinnedItemsBannerView(state: roomContext.viewState.pinnedEventsBannerState,
.transition(.move(edge: .top)) 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 { private var scrollToBottomButton: some View {
@@ -65,6 +65,7 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol {
var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) } var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) }
var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) } var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) }
var pinnedEventIDs: Set<String> { Set(roomInfo.pinnedEventIds) } var pinnedEventIDs: Set<String> { Set(roomInfo.pinnedEventIds) }
var joinRule: JoinRule? { roomInfo.joinRule }
} }
struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
+1 -1
View File
@@ -61,7 +61,7 @@ packages:
# Element/Matrix dependencies # Element/Matrix dependencies
MatrixRustSDK: MatrixRustSDK:
url: https://github.com/element-hq/matrix-rust-components-swift url: https://github.com/element-hq/matrix-rust-components-swift
exactVersion: 1.0.73 exactVersion: 1.0.74
# path: ../matrix-rust-sdk # path: ../matrix-rust-sdk
Compound: Compound:
url: https://github.com/element-hq/compound-ios url: https://github.com/element-hq/compound-ios