Skip to content

Commit d80ca29

Browse files
authored
Merge pull request #4402 from vector-im/feature/adm/notification-images
Supporting images in notifications
2 parents 183ca18 + 1eb544e commit d80ca29

File tree

8 files changed

+69
-16
lines changed

8 files changed

+69
-16
lines changed

changelog.d/4402.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Adds support for images inside message notifications

vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@ fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String {
6666
replaceRange(idx, idx, insert)
6767
}
6868
}
69+
70+
inline fun <reified R> Any?.takeAs(): R? {
71+
return takeIf { it is R } as R?
72+
}

vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package im.vector.app.features.notifications
1717

18+
import android.net.Uri
1819
import im.vector.app.BuildConfig
1920
import im.vector.app.R
21+
import im.vector.app.core.extensions.takeAs
2022
import im.vector.app.core.resources.StringProvider
2123
import im.vector.app.features.displayname.getBestName
2224
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
@@ -28,12 +30,15 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
2830
import org.matrix.android.sdk.api.session.events.model.Event
2931
import org.matrix.android.sdk.api.session.events.model.EventType
3032
import org.matrix.android.sdk.api.session.events.model.isEdition
33+
import org.matrix.android.sdk.api.session.events.model.isImageMessage
3134
import org.matrix.android.sdk.api.session.events.model.toModel
3235
import org.matrix.android.sdk.api.session.room.model.Membership
3336
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
37+
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
3438
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
3539
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
3640
import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
41+
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
3742
import org.matrix.android.sdk.api.util.toMatrixItem
3843
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
3944
import timber.log.Timber
@@ -49,11 +54,12 @@ import javax.inject.Inject
4954
class NotifiableEventResolver @Inject constructor(
5055
private val stringProvider: StringProvider,
5156
private val noticeEventFormatter: NoticeEventFormatter,
52-
private val displayableEventFormatter: DisplayableEventFormatter) {
57+
private val displayableEventFormatter: DisplayableEventFormatter
58+
) {
5359

5460
// private val eventDisplay = RiotEventDisplay(context)
5561

56-
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? {
62+
suspend fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? {
5763
val roomID = event.roomId ?: return null
5864
val eventId = event.eventId ?: return null
5965
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
@@ -89,7 +95,7 @@ class NotifiableEventResolver @Inject constructor(
8995
}
9096
}
9197

92-
fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
98+
suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
9399
if (event.getClearType() != EventType.MESSAGE) return null
94100

95101
// Ignore message edition
@@ -120,7 +126,7 @@ class NotifiableEventResolver @Inject constructor(
120126
}
121127
}
122128

123-
private fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
129+
private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
124130
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
125131
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
126132

@@ -140,6 +146,7 @@ class NotifiableEventResolver @Inject constructor(
140146
senderName = senderDisplayName,
141147
senderId = event.root.senderId,
142148
body = body.toString(),
149+
imageUri = event.fetchImageIfPresent(session),
143150
roomId = event.root.roomId!!,
144151
roomName = roomName,
145152
matrixID = session.myUserId
@@ -173,6 +180,7 @@ class NotifiableEventResolver @Inject constructor(
173180
senderName = senderDisplayName,
174181
senderId = event.root.senderId,
175182
body = body,
183+
imageUri = event.fetchImageIfPresent(session),
176184
roomId = event.root.roomId!!,
177185
roomName = roomName,
178186
roomIsDirect = room.roomSummary()?.isDirect ?: false,
@@ -192,6 +200,26 @@ class NotifiableEventResolver @Inject constructor(
192200
}
193201
}
194202

203+
private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? {
204+
return when {
205+
root.isEncrypted() && root.mxDecryptionResult == null -> null
206+
root.isImageMessage() -> downloadAndExportImage(session)
207+
else -> null
208+
}
209+
}
210+
211+
private suspend fun TimelineEvent.downloadAndExportImage(session: Session): Uri? {
212+
return kotlin.runCatching {
213+
getLastMessageContent()?.takeAs<MessageWithAttachmentContent>()?.let { imageMessage ->
214+
val fileService = session.fileService()
215+
fileService.downloadFile(imageMessage)
216+
fileService.getTemporarySharableURI(imageMessage)
217+
}
218+
}.onFailure {
219+
Timber.e(it, "Failed to download and export image for notification")
220+
}.getOrNull()
221+
}
222+
195223
private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
196224
val content = event.content?.toModel<RoomMemberContent>() ?: return null
197225
val roomId = event.roomId ?: return null

vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package im.vector.app.features.notifications
1717

18+
import android.net.Uri
1819
import org.matrix.android.sdk.api.session.events.model.EventType
1920

2021
data class NotifiableMessageEvent(
@@ -26,6 +27,7 @@ data class NotifiableMessageEvent(
2627
val senderName: String?,
2728
val senderId: String?,
2829
val body: String?,
30+
val imageUri: Uri?,
2931
val roomId: String,
3032
val roomName: String?,
3133
val roomIsDirect: Boolean = false,

vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
138138
?: context?.getString(R.string.notification_sender_me),
139139
senderId = session.myUserId,
140140
body = message,
141+
imageUri = null,
141142
roomId = room.roomId,
142143
roomName = room.roomSummary()?.displayName ?: room.roomId,
143144
roomIsDirect = room.roomSummary()?.isDirect == true,

vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
package im.vector.app.features.notifications
1818

19+
import kotlinx.coroutines.CancellationException
20+
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.SupervisorJob
22+
import kotlinx.coroutines.cancelChildren
23+
import kotlinx.coroutines.launch
1924
import org.matrix.android.sdk.api.pushrules.PushEvents
2025
import org.matrix.android.sdk.api.pushrules.PushRuleService
2126
import org.matrix.android.sdk.api.pushrules.getActions
@@ -31,21 +36,24 @@ class PushRuleTriggerListener @Inject constructor(
3136
) : PushRuleService.PushRuleListener {
3237

3338
private var session: Session? = null
39+
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
3440

3541
override fun onEvents(pushEvents: PushEvents) {
36-
session?.let { session ->
37-
val notifiableEvents = createNotifiableEvents(pushEvents, session)
38-
notificationDrawerManager.updateEvents { queuedEvents ->
39-
notifiableEvents.forEach { notifiableEvent ->
40-
queuedEvents.onNotifiableEventReceived(notifiableEvent)
42+
scope.launch {
43+
session?.let { session ->
44+
val notifiableEvents = createNotifiableEvents(pushEvents, session)
45+
notificationDrawerManager.updateEvents { queuedEvents ->
46+
notifiableEvents.forEach { notifiableEvent ->
47+
queuedEvents.onNotifiableEventReceived(notifiableEvent)
48+
}
49+
queuedEvents.syncRoomEvents(roomsLeft = pushEvents.roomsLeft, roomsJoined = pushEvents.roomsJoined)
50+
queuedEvents.markRedacted(pushEvents.redactedEventIds)
4151
}
42-
queuedEvents.syncRoomEvents(roomsLeft = pushEvents.roomsLeft, roomsJoined = pushEvents.roomsJoined)
43-
queuedEvents.markRedacted(pushEvents.redactedEventIds)
44-
}
45-
} ?: Timber.e("Called without active session")
52+
} ?: Timber.e("Called without active session")
53+
}
4654
}
4755

48-
private fun createNotifiableEvents(pushEvents: PushEvents, session: Session): List<NotifiableEvent> {
56+
private suspend fun createNotifiableEvents(pushEvents: PushEvents, session: Session): List<NotifiableEvent> {
4957
return pushEvents.matchedEvents.mapNotNull { (event, pushRule) ->
5058
Timber.v("Push rule match for event ${event.eventId}")
5159
val action = pushRule.getActions().toNotificationAction()
@@ -67,6 +75,7 @@ class PushRuleTriggerListener @Inject constructor(
6775
}
6876

6977
fun stop() {
78+
scope.coroutineContext.cancelChildren(CancellationException("PushRuleTriggerListener stopping"))
7079
session?.removePushRuleListener(this)
7180
session = null
7281
notificationDrawerManager.clearAllEvents()

vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,14 @@ class RoomGroupMessageCreator @Inject constructor(
114114
}
115115
when {
116116
event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
117-
else -> addMessage(event.body, event.timestamp, senderPerson)
117+
else -> {
118+
val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message ->
119+
event.imageUri?.let {
120+
message.setData("image/", it)
121+
}
122+
}
123+
addMessage(message)
124+
}
118125
}
119126
}
120127
}

vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ fun aNotifiableMessageEvent(
7676
roomName = "room-name",
7777
roomIsDirect = false,
7878
canBeReplaced = false,
79-
isRedacted = isRedacted
79+
isRedacted = isRedacted,
80+
imageUri = null
8081
)

0 commit comments

Comments
 (0)