Display room invitation notification (#735)

* Notifications: Add some extra mappings so we keep the original contents and can pass it later to an UI layer

* Fix notifications not appearing for a room if the app was on that room when it went to background.

* Modernize how we create spannable strings for notifications, remove unneeded dependency

* Remove actions from invite notifications temporarily

* Add `NotificationDrawerManager` interface to be able to clear membership notifications when accepting or rejecting a room invite

* Fix tests

* Add comment to clarify some weird behaviours

* Address review comments

* Set circle shape for `largeBitmap` in message notifications

* Fix no avatar in DM rooms

* Fix rebase issues

* Add invite list pending intent:

- Refactor pending intents.
- Make `DeepLinkData` a sealed interface.
- Fix and add tests.

* Rename `navigate__` functions to `attach__`

* Add an extra test case for the `InviteList` deep link

* Address most review comments.

* Fix rebase issue

* Add fallback notification type, allow dismissing invite notifications.

Fallback notifications have a different underlying type and can be dismissed at will.

* Fix tests
This commit is contained in:
Jorge Martin Espinosa 2023-07-10 14:34:58 +02:00 committed by GitHub
parent 0fbf799d15
commit a0c1f2c18a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 905 additions and 327 deletions

View file

@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
import kotlinx.collections.immutable.toPersistentList
@ -49,6 +50,7 @@ class InviteListPresenter @Inject constructor(
private val client: MatrixClient,
private val store: SeenInvitesStore,
private val analyticsService: AnalyticsService,
private val notificationDrawerManager: NotificationDrawerManager,
) : Presenter<InviteListState> {
@Composable
@ -138,6 +140,7 @@ class InviteListPresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.acceptInvitation().getOrThrow()
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
}
roomId
@ -148,7 +151,9 @@ class InviteListPresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.rejectInvitation().getOrThrow()
} ?: Unit
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
Unit
}.runCatchingUpdatingState(declinedAction)
}

View file

@ -21,10 +21,12 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.features.invitelist.api.SeenInvitesStore
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@ -39,6 +41,9 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -47,12 +52,8 @@ class InviteListPresenterTests {
@Test
fun `present - starts empty, adds invites when received`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -72,12 +73,8 @@ class InviteListPresenterTests {
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -102,12 +99,8 @@ class InviteListPresenterTests {
@Test
fun `present - includes sender details for room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -136,6 +129,7 @@ class InviteListPresenterTests {
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -155,12 +149,8 @@ class InviteListPresenterTests {
@Test
fun `present - shows confirm dialog for declining room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -180,12 +170,8 @@ class InviteListPresenterTests {
@Test
fun `present - hides confirm dialog when cancelling`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -205,11 +191,12 @@ class InviteListPresenterTests {
@Test
fun `present - declines invite after confirming`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionClock.Immediate) {
@ -225,6 +212,7 @@ class InviteListPresenterTests {
skipItems(2)
Truth.assertThat(room.isInviteRejected).isTrue()
Truth.assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
}
}
@ -235,7 +223,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenRejectInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@ -266,7 +254,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenRejectInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@ -294,11 +282,12 @@ class InviteListPresenterTests {
@Test
fun `present - accepts invites and sets state on success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionClock.Immediate) {
@ -311,6 +300,7 @@ class InviteListPresenterTests {
Truth.assertThat(room.isInviteAccepted).isTrue()
Truth.assertThat(newState.acceptedAction).isEqualTo(Async.Success(A_ROOM_ID))
Truth.assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
}
}
@ -321,7 +311,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenAcceptInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@ -346,7 +336,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenAcceptInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@ -376,6 +366,7 @@ class InviteListPresenterTests {
),
store,
FakeAnalyticsService(),
FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -413,6 +404,7 @@ class InviteListPresenterTests {
),
store,
FakeAnalyticsService(),
FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -500,4 +492,16 @@ class InviteListPresenterTests {
unreadNotificationCount = 0,
)
)
private fun createPresenter(
client: MatrixClient,
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
fakeAnalyticsService: AnalyticsService = FakeAnalyticsService(),
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager()
) = InviteListPresenter(
client,
seenInvitesStore,
fakeAnalyticsService,
notificationDrawerManager
)
}