Merge pull request #2967 from element-hq/feature/bma/notificationActions
Quick reply in notification actions
This commit is contained in:
commit
078d964b55
19 changed files with 835 additions and 237 deletions
|
|
@ -23,8 +23,10 @@ import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
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.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
|
|
@ -128,6 +130,12 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - declining invite success flow`() = runTest {
|
fun `present - declining invite success flow`() = runTest {
|
||||||
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
||||||
|
)
|
||||||
val declineInviteSuccess = lambdaRecorder { ->
|
val declineInviteSuccess = lambdaRecorder { ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +147,10 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
|
client = client,
|
||||||
|
notificationDrawerManager = notificationDrawerManager,
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val inviteData = anInviteData()
|
val inviteData = anInviteData()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
|
|
@ -159,7 +170,10 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
}
|
}
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
}
|
}
|
||||||
assert(declineInviteSuccess).isCalledOnce()
|
declineInviteSuccess.assertions().isCalledOnce()
|
||||||
|
clearMembershipNotificationForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -202,10 +216,19 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - accepting invite success flow`() = runTest {
|
fun `present - accepting invite success flow`() = runTest {
|
||||||
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
||||||
|
)
|
||||||
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
|
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomSuccess)
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
|
joinRoomLambda = joinRoomSuccess,
|
||||||
|
notificationDrawerManager = notificationDrawerManager,
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val inviteData = anInviteData()
|
val inviteData = anInviteData()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
|
|
@ -229,6 +252,9 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
value(emptyList<String>()),
|
value(emptyList<String>()),
|
||||||
value(JoinedRoom.Trigger.Invite)
|
value(JoinedRoom.Trigger.Invite)
|
||||||
)
|
)
|
||||||
|
clearMembershipNotificationForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun anInviteData(
|
private fun anInviteData(
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reply message`() = runTest {
|
fun `present - reply message`() = runTest {
|
||||||
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention> ->
|
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
|
|
@ -426,7 +426,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
assert(replyMessageLambda)
|
assert(replyMessageLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(any(), value(A_REPLY), value(A_REPLY), any())
|
.with(any(), value(A_REPLY), value(A_REPLY), any(), value(false))
|
||||||
|
|
||||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||||
Composer(
|
Composer(
|
||||||
|
|
@ -909,7 +909,7 @@ class MessageComposerPresenterTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `present - send messages with intentional mentions`() = runTest {
|
fun `present - send messages with intentional mentions`() = runTest {
|
||||||
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention> ->
|
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
|
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
|
||||||
|
|
@ -956,7 +956,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
assert(replyMessageLambda)
|
assert(replyMessageLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2))))
|
.with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2))), value(false))
|
||||||
|
|
||||||
// Check intentional mentions on edit message
|
// Check intentional mentions on edit message
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class AppMigration02Test {
|
||||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSessionVerificationSkipped = false)
|
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSessionVerificationSkipped = false)
|
||||||
val sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(
|
val sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(
|
||||||
getLambda = lambdaRecorder { _, _, -> sessionPreferencesStore },
|
getLambda = lambdaRecorder { _, _, -> sessionPreferencesStore },
|
||||||
|
removeLambda = lambdaRecorder { _ -> }
|
||||||
)
|
)
|
||||||
val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory)
|
val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,5 @@ value class ThreadId(val value: String) : Serializable {
|
||||||
|
|
||||||
override fun toString(): String = value
|
override fun toString(): String = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ThreadId.asEventId(): EventId = EventId(value)
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,13 @@ interface Timeline : AutoCloseable {
|
||||||
|
|
||||||
suspend fun enterSpecialMode(eventId: EventId?): Result<Unit>
|
suspend fun enterSpecialMode(eventId: EventId?): Result<Unit>
|
||||||
|
|
||||||
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
suspend fun replyMessage(
|
||||||
|
eventId: EventId,
|
||||||
|
body: String,
|
||||||
|
htmlBody: String?,
|
||||||
|
mentions: List<Mention>,
|
||||||
|
fromNotification: Boolean = false,
|
||||||
|
): Result<Unit>
|
||||||
|
|
||||||
suspend fun sendImage(
|
suspend fun sendImage(
|
||||||
file: File,
|
file: File,
|
||||||
|
|
|
||||||
|
|
@ -308,13 +308,28 @@ class RustTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
|
override suspend fun replyMessage(
|
||||||
|
eventId: EventId,
|
||||||
|
body: String,
|
||||||
|
htmlBody: String?,
|
||||||
|
mentions: List<Mention>,
|
||||||
|
fromNotification: Boolean,
|
||||||
|
): Result<Unit> = withContext(dispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value)
|
val msg = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map())
|
||||||
inReplyTo.use { eventTimelineItem ->
|
if (fromNotification) {
|
||||||
inner.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem)
|
// When replying from a notification, do not interfere with `specialModeEventTimelineItem`
|
||||||
|
val inReplyTo = inner.getEventTimelineItemByEventId(eventId.value)
|
||||||
|
inReplyTo.use { eventTimelineItem ->
|
||||||
|
inner.sendReply(msg, eventTimelineItem)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value)
|
||||||
|
inReplyTo.use { eventTimelineItem ->
|
||||||
|
inner.sendReply(msg, eventTimelineItem)
|
||||||
|
}
|
||||||
|
specialModeEventTimelineItem = null
|
||||||
}
|
}
|
||||||
specialModeEventTimelineItem = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,8 @@ class FakeTimeline(
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
mentions: List<Mention>,
|
||||||
) -> Result<Unit> = { _, _, _, _ ->
|
fromNotification: Boolean,
|
||||||
|
) -> Result<Unit> = { _, _, _, _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,11 +114,13 @@ class FakeTimeline(
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
mentions: List<Mention>,
|
||||||
|
fromNotification: Boolean,
|
||||||
): Result<Unit> = replyMessageLambda(
|
): Result<Unit> = replyMessageLambda(
|
||||||
eventId,
|
eventId,
|
||||||
body,
|
body,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
mentions
|
mentions,
|
||||||
|
fromNotification,
|
||||||
)
|
)
|
||||||
|
|
||||||
var sendImageLambda: (
|
var sendImageLambda: (
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,13 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||||
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
class FakeSessionPreferencesStoreFactory(
|
class FakeSessionPreferencesStoreFactory(
|
||||||
var getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> throw NotImplementedError() },
|
val getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> lambdaError() },
|
||||||
var removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> },
|
val removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> lambdaError() },
|
||||||
) : SessionPreferencesStoreFactory {
|
) : SessionPreferencesStoreFactory {
|
||||||
override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore {
|
override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore {
|
||||||
return getLambda(sessionId, sessionCoroutineScope)
|
return getLambda(sessionId, sessionCoroutineScope)
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,15 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.api.notifications
|
package io.element.android.libraries.push.api.notifications
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
interface NotificationDrawerManager {
|
interface NotificationDrawerManager {
|
||||||
|
fun clearAllMessagesEvents(sessionId: SessionId)
|
||||||
|
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId)
|
||||||
|
fun clearEvent(sessionId: SessionId, eventId: EventId)
|
||||||
|
|
||||||
fun clearMembershipNotificationForSession(sessionId: SessionId)
|
fun clearMembershipNotificationForSession(sessionId: SessionId)
|
||||||
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
|
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ dependencies {
|
||||||
testImplementation(libs.coil.test)
|
testImplementation(libs.coil.test)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.libraries.pushstore.test)
|
testImplementation(projects.libraries.pushstore.test)
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Clear all known message events for a [sessionId].
|
* Clear all known message events for a [sessionId].
|
||||||
*/
|
*/
|
||||||
fun clearAllMessagesEvents(sessionId: SessionId) {
|
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||||
notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||||
clearSummaryNotificationIfNeeded(sessionId)
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +141,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||||
* Can also be called when a notification for this room is dismissed by the user.
|
* Can also be called when a notification for this room is dismissed by the user.
|
||||||
*/
|
*/
|
||||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||||
notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||||
clearSummaryNotificationIfNeeded(sessionId)
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +164,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Clear the notifications for a single event.
|
* Clear the notifications for a single event.
|
||||||
*/
|
*/
|
||||||
fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||||
val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
|
val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||||
notificationManager.cancel(eventId.value, id)
|
notificationManager.cancel(eventId.value, id)
|
||||||
clearSummaryNotificationIfNeeded(sessionId)
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,13 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util class for creating notifications.
|
* Util class for creating notifications action Ids, using the application id.
|
||||||
* Note: Cannot inject ColorProvider in the constructor, because it requires an Activity
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
data class NotificationActionIds @Inject constructor(
|
data class NotificationActionIds @Inject constructor(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) {
|
) {
|
||||||
val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION"
|
val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION"
|
||||||
val reject = "${buildMeta.applicationId}.NotificationActions.REJECT_ACTION"
|
val reject = "${buildMeta.applicationId}.NotificationActions.REJECT_ACTION"
|
||||||
val quickLaunch = "${buildMeta.applicationId}.NotificationActions.QUICK_LAUNCH_ACTION"
|
|
||||||
val markRoomRead = "${buildMeta.applicationId}.NotificationActions.MARK_ROOM_READ_ACTION"
|
val markRoomRead = "${buildMeta.applicationId}.NotificationActions.MARK_ROOM_READ_ACTION"
|
||||||
val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION"
|
val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION"
|
||||||
val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION"
|
val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION"
|
||||||
|
|
@ -37,5 +34,4 @@ data class NotificationActionIds @Inject constructor(
|
||||||
val dismissInvite = "${buildMeta.applicationId}.NotificationActions.DISMISS_INVITE_NOTIF_ACTION"
|
val dismissInvite = "${buildMeta.applicationId}.NotificationActions.DISMISS_INVITE_NOTIF_ACTION"
|
||||||
val dismissEvent = "${buildMeta.applicationId}.NotificationActions.DISMISS_EVENT_NOTIF_ACTION"
|
val dismissEvent = "${buildMeta.applicationId}.NotificationActions.DISMISS_EVENT_NOTIF_ACTION"
|
||||||
val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC"
|
val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC"
|
||||||
val push = "${buildMeta.applicationId}.PUSH"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,211 +19,21 @@ package io.element.android.libraries.push.impl.notifications
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.).
|
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.).
|
||||||
*/
|
*/
|
||||||
class NotificationBroadcastReceiver : BroadcastReceiver() {
|
class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||||
@Inject lateinit var appCoroutineScope: CoroutineScope
|
@Inject lateinit var notificationBroadcastReceiverHandler: NotificationBroadcastReceiverHandler
|
||||||
@Inject lateinit var matrixClientProvider: MatrixClientProvider
|
|
||||||
@Inject lateinit var sessionPreferencesStore: SessionPreferencesStoreFactory
|
|
||||||
@Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager
|
|
||||||
@Inject lateinit var actionIds: NotificationActionIds
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (intent == null || context == null) return
|
if (intent == null || context == null) return
|
||||||
val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return
|
|
||||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
|
|
||||||
val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId)
|
|
||||||
|
|
||||||
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
|
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
|
||||||
|
notificationBroadcastReceiverHandler.onReceive(intent)
|
||||||
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
|
|
||||||
when (intent.action) {
|
|
||||||
actionIds.smartReply ->
|
|
||||||
handleSmartReply(intent, context)
|
|
||||||
actionIds.dismissRoom -> if (roomId != null) {
|
|
||||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
|
||||||
}
|
|
||||||
actionIds.dismissSummary ->
|
|
||||||
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
|
|
||||||
actionIds.dismissInvite -> if (roomId != null) {
|
|
||||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
|
||||||
}
|
|
||||||
actionIds.dismissEvent -> if (eventId != null) {
|
|
||||||
defaultNotificationDrawerManager.clearEvent(sessionId, eventId)
|
|
||||||
}
|
|
||||||
actionIds.markRoomRead -> if (roomId != null) {
|
|
||||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
|
||||||
handleMarkAsRead(sessionId, roomId)
|
|
||||||
}
|
|
||||||
actionIds.join -> if (roomId != null) {
|
|
||||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
|
||||||
handleJoinRoom(sessionId, roomId)
|
|
||||||
}
|
|
||||||
actionIds.reject -> if (roomId != null) {
|
|
||||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
|
||||||
handleRejectRoom(sessionId, roomId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
|
||||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
|
||||||
client.joinRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
|
||||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
|
||||||
client.getRoom(roomId)?.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
|
||||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
|
||||||
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
|
|
||||||
val receiptType = if (isSendPublicReadReceiptsEnabled) {
|
|
||||||
ReceiptType.READ
|
|
||||||
} else {
|
|
||||||
ReceiptType.READ_PRIVATE
|
|
||||||
}
|
|
||||||
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
private fun handleSmartReply(intent: Intent, context: Context) {
|
|
||||||
/*
|
|
||||||
val message = getReplyMessage(intent)
|
|
||||||
val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.let(::SessionId)
|
|
||||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
|
|
||||||
val threadId = intent.getStringExtra(KEY_THREAD_ID)?.let(::ThreadId)
|
|
||||||
|
|
||||||
if (message.isNullOrBlank() || roomId == null) {
|
|
||||||
// ignore this event
|
|
||||||
// Can this happen? should we update notification?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
activeSessionHolder.getActiveSession().let { session ->
|
|
||||||
session.getRoom(roomId)?.let { room ->
|
|
||||||
sendMatrixEvent(message, threadId, session, room, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) {
|
|
||||||
if (threadId != null) {
|
|
||||||
room.relationService().replyInThread(
|
|
||||||
rootThreadEventId = threadId,
|
|
||||||
replyInThreadText = message,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
room.sendService().sendTextMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new event to be displayed in the notification drawer, right now
|
|
||||||
|
|
||||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
|
||||||
// Generate a Fake event id
|
|
||||||
eventId = UUID.randomUUID().toString(),
|
|
||||||
editedEventId = null,
|
|
||||||
noisy = false,
|
|
||||||
timestamp = clock.epochMillis(),
|
|
||||||
senderName = session.roomService().getRoomMember(session.myUserId, room.roomId)?.displayName
|
|
||||||
?: context?.getString(R.string.notification_sender_me),
|
|
||||||
senderId = session.myUserId,
|
|
||||||
body = message,
|
|
||||||
imageUriString = null,
|
|
||||||
roomId = room.roomId,
|
|
||||||
threadId = threadId,
|
|
||||||
roomName = room.roomSummary()?.displayName ?: room.roomId,
|
|
||||||
roomIsDirect = room.roomSummary()?.isDirect == true,
|
|
||||||
outGoingMessage = true,
|
|
||||||
canBeReplaced = false
|
|
||||||
)
|
|
||||||
|
|
||||||
notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) }
|
|
||||||
|
|
||||||
/*
|
|
||||||
// TODO Error cannot be managed the same way than in Riot
|
|
||||||
|
|
||||||
val event = Event(mxMessage, session.credentials.userId, roomId)
|
|
||||||
room.storeOutgoingEvent(event)
|
|
||||||
room.sendEvent(event, object : MatrixCallback<Void?> {
|
|
||||||
override fun onSuccess(info: Void?) {
|
|
||||||
Timber.v("Send message : onSuccess ")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNetworkError(e: Exception) {
|
|
||||||
Timber.e(e, "Send message : onNetworkError")
|
|
||||||
onSmartReplyFailed(e.localizedMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMatrixError(e: MatrixError) {
|
|
||||||
Timber.v("Send message : onMatrixError " + e.message)
|
|
||||||
if (e is MXCryptoError) {
|
|
||||||
Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show()
|
|
||||||
onSmartReplyFailed(e.detailedErrorDescription)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
|
|
||||||
onSmartReplyFailed(e.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnexpectedError(e: Exception) {
|
|
||||||
Timber.e(e, "Send message : onUnexpectedError " + e.message)
|
|
||||||
onSmartReplyFailed(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onSmartReplyFailed(reason: String?) {
|
|
||||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
|
||||||
event.eventId,
|
|
||||||
false,
|
|
||||||
clock.epochMillis(),
|
|
||||||
session.myUser?.displayname
|
|
||||||
?: context?.getString(R.string.notification_sender_me),
|
|
||||||
session.myUserId,
|
|
||||||
message,
|
|
||||||
roomId,
|
|
||||||
room.getRoomDisplayName(context),
|
|
||||||
room.isDirect)
|
|
||||||
notifiableMessageEvent.outGoingMessage = true
|
|
||||||
notifiableMessageEvent.outGoingMessageFailed = true
|
|
||||||
|
|
||||||
VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
|
||||||
VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getReplyMessage(intent: Intent?): String? {
|
|
||||||
if (intent != null) {
|
|
||||||
val remoteInput = RemoteInput.getResultsFromIntent(intent)
|
|
||||||
if (remoteInput != null) {
|
|
||||||
return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_SESSION_ID = "sessionID"
|
const val KEY_SESSION_ID = "sessionID"
|
||||||
const val KEY_ROOM_ID = "roomID"
|
const val KEY_ROOM_ID = "roomID"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
import io.element.android.libraries.matrix.api.core.asEventId
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
|
import io.element.android.libraries.push.impl.R
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
|
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||||
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag)
|
||||||
|
|
||||||
|
class NotificationBroadcastReceiverHandler @Inject constructor(
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStoreFactory,
|
||||||
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
|
private val actionIds: NotificationActionIds,
|
||||||
|
private val systemClock: SystemClock,
|
||||||
|
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val replyMessageExtractor: ReplyMessageExtractor,
|
||||||
|
) {
|
||||||
|
fun onReceive(intent: Intent) {
|
||||||
|
val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||||
|
val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId)
|
||||||
|
val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId)
|
||||||
|
val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId)
|
||||||
|
|
||||||
|
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
|
||||||
|
when (intent.action) {
|
||||||
|
actionIds.smartReply -> if (roomId != null) {
|
||||||
|
handleSmartReply(sessionId, roomId, threadId, intent)
|
||||||
|
}
|
||||||
|
actionIds.dismissRoom -> if (roomId != null) {
|
||||||
|
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||||
|
}
|
||||||
|
actionIds.dismissSummary ->
|
||||||
|
notificationDrawerManager.clearAllMessagesEvents(sessionId)
|
||||||
|
actionIds.dismissInvite -> if (roomId != null) {
|
||||||
|
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||||
|
}
|
||||||
|
actionIds.dismissEvent -> if (eventId != null) {
|
||||||
|
notificationDrawerManager.clearEvent(sessionId, eventId)
|
||||||
|
}
|
||||||
|
actionIds.markRoomRead -> if (roomId != null) {
|
||||||
|
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||||
|
handleMarkAsRead(sessionId, roomId)
|
||||||
|
}
|
||||||
|
actionIds.join -> if (roomId != null) {
|
||||||
|
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||||
|
handleJoinRoom(sessionId, roomId)
|
||||||
|
}
|
||||||
|
actionIds.reject -> if (roomId != null) {
|
||||||
|
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||||
|
handleRejectRoom(sessionId, roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||||
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||||
|
client.joinRoom(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||||
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||||
|
client.getRoom(roomId)?.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||||
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||||
|
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
|
||||||
|
val receiptType = if (isSendPublicReadReceiptsEnabled) {
|
||||||
|
ReceiptType.READ
|
||||||
|
} else {
|
||||||
|
ReceiptType.READ_PRIVATE
|
||||||
|
}
|
||||||
|
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSmartReply(
|
||||||
|
sessionId: SessionId,
|
||||||
|
roomId: RoomId,
|
||||||
|
threadId: ThreadId?,
|
||||||
|
intent: Intent,
|
||||||
|
) = appCoroutineScope.launch {
|
||||||
|
val message = replyMessageExtractor.getReplyMessage(intent)
|
||||||
|
if (message.isNullOrBlank()) {
|
||||||
|
// ignore this event
|
||||||
|
// Can this happen? should we update notification?
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||||
|
client.getRoom(roomId)?.let { room ->
|
||||||
|
sendMatrixEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
roomId = roomId,
|
||||||
|
threadId = threadId,
|
||||||
|
room = room,
|
||||||
|
message = message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendMatrixEvent(
|
||||||
|
sessionId: SessionId,
|
||||||
|
roomId: RoomId,
|
||||||
|
threadId: ThreadId?,
|
||||||
|
room: MatrixRoom,
|
||||||
|
message: String,
|
||||||
|
) {
|
||||||
|
// Create a new event to be displayed in the notification drawer, right now
|
||||||
|
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
roomId = roomId,
|
||||||
|
// Generate a Fake event id
|
||||||
|
eventId = EventId("\$" + UUID.randomUUID().toString()),
|
||||||
|
editedEventId = null,
|
||||||
|
canBeReplaced = false,
|
||||||
|
senderId = sessionId,
|
||||||
|
noisy = false,
|
||||||
|
timestamp = systemClock.epochMillis(),
|
||||||
|
senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull()
|
||||||
|
?.disambiguatedDisplayName
|
||||||
|
?: stringProvider.getString(R.string.notification_sender_me),
|
||||||
|
body = message,
|
||||||
|
imageUriString = null,
|
||||||
|
threadId = threadId,
|
||||||
|
roomName = room.displayName,
|
||||||
|
roomIsDirect = room.isDirect,
|
||||||
|
outGoingMessage = true,
|
||||||
|
)
|
||||||
|
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)
|
||||||
|
|
||||||
|
if (threadId != null) {
|
||||||
|
room.liveTimeline.replyMessage(
|
||||||
|
eventId = threadId.asEventId(),
|
||||||
|
body = message,
|
||||||
|
htmlBody = null,
|
||||||
|
mentions = emptyList(),
|
||||||
|
fromNotification = true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
room.liveTimeline.sendMessage(
|
||||||
|
body = message,
|
||||||
|
htmlBody = null,
|
||||||
|
mentions = emptyList()
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to send smart reply message")
|
||||||
|
onNotifiableEventReceived.onNotifiableEventReceived(
|
||||||
|
notifiableMessageEvent.copy(
|
||||||
|
outGoingMessageFailed = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.app.RemoteInput
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface ReplyMessageExtractor {
|
||||||
|
fun getReplyMessage(intent: Intent): String?
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class AndroidReplyMessageExtractor @Inject constructor() : ReplyMessageExtractor {
|
||||||
|
override fun getReplyMessage(intent: Intent): String? {
|
||||||
|
return RemoteInput.getResultsFromIntent(intent)
|
||||||
|
?.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||||
|
?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
class FakeReplyMessageExtractor(
|
||||||
|
private val result: String? = null,
|
||||||
|
) : ReplyMessageExtractor {
|
||||||
|
override fun getReplyMessage(intent: Intent): String? {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,474 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
import io.element.android.libraries.matrix.api.core.asEventId
|
||||||
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
|
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||||
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
|
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
||||||
|
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||||
|
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||||
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class NotificationBroadcastReceiverHandlerTest {
|
||||||
|
private val actionIds = NotificationActionIds(aBuildMeta())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When no sessionId, nothing happen`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.join,
|
||||||
|
sessionId = null
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss room without a roomId, nothing happen`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissRoom,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss room`() = runTest {
|
||||||
|
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
notificationDrawerManager = notificationDrawerManager
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissRoom,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
clearMessagesForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss summary`() = runTest {
|
||||||
|
val clearAllMessagesEventsLambda = lambdaRecorder<SessionId, Unit> { _ -> }
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearAllMessagesEventsLambda = clearAllMessagesEventsLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
notificationDrawerManager = notificationDrawerManager
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissSummary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
clearAllMessagesEventsLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss Invite without room`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissInvite,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss Invite`() = runTest {
|
||||||
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
notificationDrawerManager = notificationDrawerManager
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissInvite,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
clearMembershipNotificationForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss Event without event`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissEvent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test dismiss Event`() = runTest {
|
||||||
|
val clearEventLambda = lambdaRecorder<SessionId, EventId, Unit> { _, _ -> }
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearEventLambda = clearEventLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
notificationDrawerManager = notificationDrawerManager
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.dismissEvent,
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
clearEventLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(AN_EVENT_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test mark room as read without room`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.markRoomRead,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test mark room as read, send public RR`() {
|
||||||
|
testMarkRoomAsRead(
|
||||||
|
isSendPublicReadReceiptsEnabled = true,
|
||||||
|
expectedReceiptType = ReceiptType.READ
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test mark room as read, send private RR`() {
|
||||||
|
testMarkRoomAsRead(
|
||||||
|
isSendPublicReadReceiptsEnabled = false,
|
||||||
|
expectedReceiptType = ReceiptType.READ_PRIVATE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testMarkRoomAsRead(
|
||||||
|
isSendPublicReadReceiptsEnabled: Boolean,
|
||||||
|
expectedReceiptType: ReceiptType,
|
||||||
|
) = runTest {
|
||||||
|
val getLambda = lambdaRecorder<SessionId, CoroutineScope, SessionPreferencesStore> { _, _ ->
|
||||||
|
InMemorySessionPreferencesStore(
|
||||||
|
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val sessionPreferencesStore = FakeSessionPreferencesStoreFactory(
|
||||||
|
getLambda = getLambda
|
||||||
|
)
|
||||||
|
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
val matrixRoom = FakeMatrixRoom()
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
matrixRoom = matrixRoom,
|
||||||
|
notificationDrawerManager = notificationDrawerManager
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.markRoomRead,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
clearMessagesForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
|
assertThat(matrixRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test join room without room`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.join,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test join room`() = runTest {
|
||||||
|
val joinRoom = lambdaRecorder<RoomId, Result<Unit>> { _ -> Result.success(Unit) }
|
||||||
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
joinRoom = joinRoom,
|
||||||
|
notificationDrawerManager = notificationDrawerManager,
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.join,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
joinRoom.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
clearMembershipNotificationForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test reject room without room`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.reject,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test reject room`() = runTest {
|
||||||
|
val leaveRoom = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val matrixRoom = FakeMatrixRoom().apply {
|
||||||
|
leaveRoomLambda = leaveRoom
|
||||||
|
}
|
||||||
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||||
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
matrixRoom = matrixRoom,
|
||||||
|
notificationDrawerManager = notificationDrawerManager
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.reject,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
clearMembershipNotificationForRoomLambda.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
|
leaveRoom.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test send reply without room`() = runTest {
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler()
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.smartReply,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test send reply`() = runTest {
|
||||||
|
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||||
|
val replyMessage = lambdaRecorder<EventId, String, String?, List<Mention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||||
|
val liveTimeline = FakeTimeline().apply {
|
||||||
|
sendMessageLambda = sendMessage
|
||||||
|
replyMessageLambda = replyMessage
|
||||||
|
}
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
liveTimeline = liveTimeline
|
||||||
|
)
|
||||||
|
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
|
||||||
|
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
matrixRoom = matrixRoom,
|
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
|
replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE)
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.smartReply,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
sendMessage.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_MESSAGE), value(null), value(emptyList<Mention>()))
|
||||||
|
onNotifiableEventReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
replyMessage.assertions()
|
||||||
|
.isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test send reply blank message`() = runTest {
|
||||||
|
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||||
|
val liveTimeline = FakeTimeline().apply {
|
||||||
|
sendMessageLambda = sendMessage
|
||||||
|
}
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
liveTimeline = liveTimeline
|
||||||
|
)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
matrixRoom = matrixRoom,
|
||||||
|
replyMessageExtractor = FakeReplyMessageExtractor(" "),
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.smartReply,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
sendMessage.assertions()
|
||||||
|
.isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test send reply to thread`() = runTest {
|
||||||
|
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||||
|
val replyMessage = lambdaRecorder<EventId, String, String?, List<Mention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||||
|
val liveTimeline = FakeTimeline().apply {
|
||||||
|
sendMessageLambda = sendMessage
|
||||||
|
replyMessageLambda = replyMessage
|
||||||
|
}
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
liveTimeline = liveTimeline
|
||||||
|
)
|
||||||
|
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
|
||||||
|
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
|
||||||
|
val sut = createNotificationBroadcastReceiverHandler(
|
||||||
|
matrixRoom = matrixRoom,
|
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
|
replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE)
|
||||||
|
)
|
||||||
|
sut.onReceive(
|
||||||
|
createIntent(
|
||||||
|
action = actionIds.smartReply,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = A_THREAD_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
sendMessage.assertions()
|
||||||
|
.isNeverCalled()
|
||||||
|
onNotifiableEventReceivedResult.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
replyMessage.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_THREAD_ID.asEventId()), value(A_MESSAGE), value(null), value(emptyList<Mention>()), value(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIntent(
|
||||||
|
action: String,
|
||||||
|
sessionId: SessionId? = A_SESSION_ID,
|
||||||
|
roomId: RoomId? = null,
|
||||||
|
eventId: EventId? = null,
|
||||||
|
threadId: ThreadId? = null,
|
||||||
|
) = Intent(action).apply {
|
||||||
|
putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId?.value)
|
||||||
|
putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId?.value)
|
||||||
|
putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, threadId?.value)
|
||||||
|
putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, eventId?.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createNotificationBroadcastReceiverHandler(
|
||||||
|
matrixRoom: FakeMatrixRoom? = FakeMatrixRoom(),
|
||||||
|
joinRoom: (RoomId) -> Result<Unit> = { lambdaError() },
|
||||||
|
matrixClient: MatrixClient? = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, matrixRoom)
|
||||||
|
joinRoomLambda = joinRoom
|
||||||
|
},
|
||||||
|
sessionPreferencesStore: SessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
|
||||||
|
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
|
||||||
|
systemClock: SystemClock = FakeSystemClock(),
|
||||||
|
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||||
|
stringProvider: StringProvider = FakeStringProvider(),
|
||||||
|
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||||
|
): NotificationBroadcastReceiverHandler {
|
||||||
|
return NotificationBroadcastReceiverHandler(
|
||||||
|
appCoroutineScope = this,
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider {
|
||||||
|
if (matrixClient == null) {
|
||||||
|
Result.failure(Exception("No matrix client"))
|
||||||
|
} else {
|
||||||
|
Result.success(matrixClient)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
notificationDrawerManager = notificationDrawerManager,
|
||||||
|
actionIds = actionIds,
|
||||||
|
systemClock = systemClock,
|
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
|
stringProvider = stringProvider,
|
||||||
|
replyMessageExtractor = replyMessageExtractor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,9 +17,10 @@
|
||||||
package io.element.android.libraries.push.impl.push
|
package io.element.android.libraries.push.impl.push
|
||||||
|
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeOnNotifiableEventReceived(
|
class FakeOnNotifiableEventReceived(
|
||||||
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit,
|
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||||
) : OnNotifiableEventReceived {
|
) : OnNotifiableEventReceived {
|
||||||
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||||
onNotifiableEventReceivedResult(notifiableEvent)
|
onNotifiableEventReceivedResult(notifiableEvent)
|
||||||
|
|
|
||||||
|
|
@ -16,33 +16,36 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.test.notifications
|
package io.element.android.libraries.push.test.notifications
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeNotificationDrawerManager : NotificationDrawerManager {
|
class FakeNotificationDrawerManager(
|
||||||
private val clearMemberShipNotificationForSessionCallsCount = mutableMapOf<String, Int>()
|
private val clearAllMessagesEventsLambda: (SessionId) -> Unit = { lambdaError() },
|
||||||
private val clearMemberShipNotificationForRoomCallsCount = mutableMapOf<String, Int>()
|
private val clearMessagesForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() },
|
||||||
|
private val clearEventLambda: (SessionId, EventId) -> Unit = { _, _ -> lambdaError() },
|
||||||
|
private val clearMembershipNotificationForSessionLambda: (SessionId) -> Unit = { lambdaError() },
|
||||||
|
private val clearMembershipNotificationForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() }
|
||||||
|
) : NotificationDrawerManager {
|
||||||
|
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||||
|
clearAllMessagesEventsLambda(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||||
|
clearMessagesForRoomLambda(sessionId, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||||
|
clearEventLambda(sessionId, eventId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
|
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
|
||||||
clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value }
|
clearMembershipNotificationForSessionLambda(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||||
val key = getMembershipNotificationKey(sessionId, roomId)
|
clearMembershipNotificationForRoomLambda(sessionId, roomId)
|
||||||
clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getClearMembershipNotificationForSessionCount(sessionId: SessionId): Int {
|
|
||||||
return clearMemberShipNotificationForRoomCallsCount[sessionId.value] ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getClearMembershipNotificationForRoomCount(sessionId: SessionId, roomId: RoomId): Int {
|
|
||||||
val key = getMembershipNotificationKey(sessionId, roomId)
|
|
||||||
return clearMemberShipNotificationForRoomCallsCount[key] ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMembershipNotificationKey(sessionId: SessionId, roomId: RoomId): String {
|
|
||||||
return "$sessionId-$roomId"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue