Cleanup notification for redacted event.

This commit is contained in:
Benoit Marty 2024-08-20 15:14:49 +02:00 committed by Benoit Marty
parent 43d619217c
commit d867a5fe6f
12 changed files with 466 additions and 153 deletions

View file

@ -50,8 +50,8 @@ import io.element.android.libraries.matrix.ui.messages.toPlainText
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
@ -67,7 +67,7 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/
interface NotifiableEventResolver {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent?
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent?
}
@ContributesBinding(AppScope::class)
@ -80,7 +80,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
private val permalinkParser: PermalinkParser,
private val callNotificationEventResolver: CallNotificationEventResolver,
) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
// Restore session
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val notificationService = client.notificationService()
@ -99,8 +99,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
private suspend fun NotificationData.asNotifiableEvent(
client: MatrixClient,
userId: SessionId,
): NotifiableEvent? {
return when (val content = this.content) {
): ResolvedPushEvent? {
val content = this.content
val notifiableEvent = when (content) {
is NotificationContent.MessageLike.RoomMessage -> {
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
@ -204,8 +205,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
}
is NotificationContent.MessageLike.RoomRedaction -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for redaction")
is NotificationContent.MessageLike.RoomRedaction -> {
// Note: this case will be handled below
null
}
NotificationContent.MessageLike.Sticker -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
@ -233,6 +235,25 @@ class DefaultNotifiableEventResolver @Inject constructor(
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
}
}
return if (notifiableEvent != null) {
ResolvedPushEvent.Event(notifiableEvent)
} else if (content is NotificationContent.MessageLike.RoomRedaction) {
val redactedEventId = content.redactedEventId
if (redactedEventId == null) {
Timber.tag(loggerTag.value).d("redactedEventId is null.")
null
} else {
ResolvedPushEvent.Redaction(
sessionId = userId,
roomId = roomId,
redactedEventId = redactedEventId,
reason = content.reason,
)
}
} else {
null
}
}
private fun fallbackNotifiableEvent(

View file

@ -430,6 +430,7 @@ class DefaultNotificationCreator @Inject constructor(
event.imageUri?.let {
message.setData("image/", it)
}
message.extras.putString(MESSAGE_EVENT_ID, event.eventId.value)
}
addMessage(message)
}
@ -465,6 +466,10 @@ class DefaultNotificationCreator @Inject constructor(
drawable.draw(canvas)
return bitmap
}
companion object {
const val MESSAGE_EVENT_ID = "message_event_id"
}
}
fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed

View file

@ -0,0 +1,32 @@
/*
* 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
*
* https://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.model
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
sealed interface ResolvedPushEvent {
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent
data class Redaction(
val sessionId: SessionId,
val roomId: RoomId,
val redactedEventId: EventId,
val reason: String?,
) : ResolvedPushEvent
}

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
@ -41,6 +42,7 @@ private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor(
private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val onRedactedEventReceived: OnRedactedEventReceived,
private val notifiableEventResolver: NotifiableEventResolver,
private val incrementPushDataStore: IncrementPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory,
@ -96,19 +98,26 @@ class DefaultPushHandler @Inject constructor(
Timber.w("Unable to get a session")
return
}
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
when (notifiableEvent) {
val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
when (resolvedPushEvent) {
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
else -> {
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
is ResolvedPushEvent.Event -> {
when (resolvedPushEvent.notifiableEvent) {
is NotifiableRingingCallEvent -> handleRingingCallEvent(resolvedPushEvent.notifiableEvent)
else -> {
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(resolvedPushEvent.notifiableEvent)
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
}
}
}
}
is ResolvedPushEvent.Redaction -> {
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
}
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")

View file

@ -0,0 +1,97 @@
/*
* 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.push
import android.content.Context
import android.graphics.Typeface
import android.text.style.StyleSpan
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreator
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
interface OnRedactedEventReceived {
fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction)
}
@ContributesBinding(AppScope::class)
class DefaultOnRedactedEventReceived @Inject constructor(
private val activeNotificationsProvider: ActiveNotificationsProvider,
private val notificationDisplayer: NotificationDisplayer,
private val coroutineScope: CoroutineScope,
@ApplicationContext private val context: Context,
private val stringProvider: StringProvider,
) : OnRedactedEventReceived {
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
coroutineScope.launch {
val notifications = activeNotificationsProvider.getMessageNotificationsForRoom(
redaction.sessionId,
redaction.roomId,
)
if (notifications.isEmpty()) {
Timber.d("No notifications found for redacted event")
}
notifications.forEach { statusBarNotification ->
val notification = statusBarNotification.notification
val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification)
if (messagingStyle == null) {
Timber.w("Unable to retrieve messaging style from notification")
return@forEach
}
val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message ->
message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) == redaction.redactedEventId.value
}
if (messageToRedactIndex == -1) {
Timber.d("Unable to find the message to remove from notification")
return@forEach
}
val oldMessage = messagingStyle.messages[messageToRedactIndex]
val content = buildSpannedString {
inSpans(StyleSpan(Typeface.ITALIC)) {
append(stringProvider.getString(CommonStrings.common_message_removed))
}
}
val newMessage = MessagingStyle.Message(
content,
oldMessage.timestamp,
oldMessage.person
)
messagingStyle.messages[messageToRedactIndex] = newMessage
notificationDisplayer.showNotificationMessage(
statusBarNotification.tag,
statusBarNotification.id,
NotificationCompat.Builder(context, notification)
.setStyle(messagingStyle)
.build()
)
}
}
}
}