Repository: element-hq/element-x-android (language: kotlin)

Issue:
bring "mark as read" button on the notification back
### Your use case

Please bring "mark as read" button on the notification back.

![image](https://github.com/user-attachments/assets/18f2a8fa-104c-4ee4-8d4f-1d1cca709a0c)


### Have you considered any alternatives?

_No response_

### Additional context

_No response_

### Are you willing to provide a PR?

No
have quick reply on notifs that when you reply via the notif it replies to the message in the room
### Your use case

#### What would you like to do?
when you reply to a notification of a message it replies to the message in element x
#### Why would you like to do it?
its nice to quickly reply to a message without opening the app and possible getting sidetracked by other rooms or conversations
#### How would you like to achieve it?
have a quick reply option when a notification gets sent like in ele android, that replies to the message in the notification, which isnt how ele android does it currently

### Have you considered any alternatives?

you could just send the message while not replying but that can easily cause confusion in a busy room

### Additional context

_No response_

### Are you willing to provide a PR?

No


Discussion hints:
Thanks for creating the issue. We are waiting for a product decision to restore notification actions.
I agree it would be nice to support this. It's currently disabled, waiting for a product decision.
Would be great
See also #1547

Pull request: Enable all notification actions: quick reply, accept/decline invite, mark as read from notification.
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Content

<!-- Describe shortly what has been changed -->
Enable all notification actions: quick reply, accept/decline invite, mark as read from notification.

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #3908
Closes #1966

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

![Notification action](https://github.com/user-attachments/assets/8e417828-8346-4252-b14d-6c09967472e8)

## Tests

<!-- Explain how you tested your development -->

- Receive notification for message or for room invite and see that they now have actions.

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR


Test command: ./gradlew :libraries:push:impl:testDebugUnitTest --tests "io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreatorTest" --tests "io.element.android.libraries.push.impl.notifications.DefaultRoomGroupMessageCreatorTest"

Fix patch:
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt
index c5f605e5fe3..83ea9e3b77d 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt
@@ -11,14 +11,20 @@ import android.graphics.Color
 import androidx.annotation.ColorInt
 
 object NotificationConfig {
-    // TODO EAx Implement and set to true at some point
-    const val SUPPORT_MARK_AS_READ_ACTION = false
+    /**
+     * If set to true, the notification will have a "Mark as read" action.
+     */
+    const val SHOW_MARK_AS_READ_ACTION = true
 
-    // TODO EAx Implement and set to true at some point
-    const val SUPPORT_JOIN_DECLINE_INVITE = false
+    /**
+     * If set to true, the notification for invitation will have two actions to accept or decline the invite.
+     */
+    const val SHOW_ACCEPT_AND_DECLINE_INVITE_ACTIONS = true
 
-    // TODO EAx Implement and set to true at some point
-    const val SUPPORT_QUICK_REPLY_ACTION = false
+    /**
+     * If set to true, the notification will have a "Quick reply" action, allow to compose and send a message to the room.
+     */
+    const val SHOW_QUICK_REPLY_ACTION = true
 
     @ColorInt
     val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt
index 559322eaaef..6788410468a 100755
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt
@@ -230,10 +230,8 @@ class DefaultNotificationCreator @Inject constructor(
             .setSmallIcon(smallIcon)
             .setColor(accentColor)
             .apply {
-                if (NotificationConfig.SUPPORT_JOIN_DECLINE_INVITE) {
-                    addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
-                    addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
-                }
+                addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
+                addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
                 // Build the pending intent for when the notification is clicked
                 setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(inviteNotifiableEvent.sessionId, inviteNotifiableEvent.roomId))
 
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt
index ee3d392c1fb..9b2cf64a7d4 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt
@@ -11,12 +11,14 @@ import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import androidx.core.app.NotificationCompat
+import io.element.android.appconfig.NotificationConfig
 import io.element.android.libraries.androidutils.uri.createIgnoredUri
 import io.element.android.libraries.di.ApplicationContext
 import io.element.android.libraries.push.impl.R
 import io.element.android.libraries.push.impl.notifications.NotificationActionIds
 import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver
 import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
+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
 import javax.inject.Inject
@@ -27,8 +29,8 @@ class AcceptInvitationActionFactory @Inject constructor(
     private val stringProvider: StringProvider,
     private val clock: SystemClock,
 ) {
-    // offer to type a quick accept button
-    fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action {
+    fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action? {
+        if (!NotificationConfig.SHOW_ACCEPT_AND_DECLINE_INVITE_ACTIONS) return null
         val sessionId = inviteNotifiableEvent.sessionId.value
         val roomId = inviteNotifiableEvent.roomId.value
         val intent = Intent(context, NotificationBroadcastReceiver::class.java)
@@ -44,7 +46,7 @@ class AcceptInvitationActionFactory @Inject constructor(
         )
         return NotificationCompat.Action.Builder(
             R.drawable.vector_notification_accept_invitation,
-            stringProvider.getString(R.string.notification_invitation_action_join),
+            stringProvider.getString(CommonStrings.action_accept),
             pendingIntent
         ).build()
     }
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt
index b77e36cfc00..e6a2594bb83 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt
@@ -29,7 +29,7 @@ class MarkAsReadActionFactory @Inject constructor(
     private val clock: SystemClock,
 ) {
     fun create(roomInfo: RoomEventGroupInfo): NotificationCompat.Action? {
-        if (!NotificationConfig.SUPPORT_MARK_AS_READ_ACTION) return null
+        if (!NotificationConfig.SHOW_MARK_AS_READ_ACTION) return null
         val sessionId = roomInfo.sessionId.value
         val roomId = roomInfo.roomId.value
         val intent = Intent(context, NotificationBroadcastReceiver::class.java)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt
index 3ada849026a..bced0abd904 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt
@@ -34,7 +34,7 @@ class QuickReplyActionFactory @Inject constructor(
     private val clock: SystemClock,
 ) {
     fun create(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): NotificationCompat.Action? {
-        if (!NotificationConfig.SUPPORT_QUICK_REPLY_ACTION) return null
+        if (!NotificationConfig.SHOW_QUICK_REPLY_ACTION) return null
         val sessionId = roomInfo.sessionId
         val roomId = roomInfo.roomId
         val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, threadId)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt
index 309acce44cf..d4eb28f1411 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt
@@ -11,12 +11,14 @@ import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import androidx.core.app.NotificationCompat
+import io.element.android.appconfig.NotificationConfig
 import io.element.android.libraries.androidutils.uri.createIgnoredUri
 import io.element.android.libraries.di.ApplicationContext
 import io.element.android.libraries.push.impl.R
 import io.element.android.libraries.push.impl.notifications.NotificationActionIds
 import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver
 import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
+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
 import javax.inject.Inject
@@ -28,6 +30,7 @@ class RejectInvitationActionFactory @Inject constructor(
     private val clock: SystemClock,
 ) {
     fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action? {
+        if (!NotificationConfig.SHOW_ACCEPT_AND_DECLINE_INVITE_ACTIONS) return null
         val sessionId = inviteNotifiableEvent.sessionId.value
         val roomId = inviteNotifiableEvent.roomId.value
         val intent = Intent(context, NotificationBroadcastReceiver::class.java)
@@ -41,10 +44,9 @@ class RejectInvitationActionFactory @Inject constructor(
             intent,
             PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
         )
-
         return NotificationCompat.Action.Builder(
             R.drawable.vector_notification_reject_invitation,
-            stringProvider.getString(R.string.notification_invitation_action_reject),
+            stringProvider.getString(CommonStrings.action_reject),
             pendingIntent
         ).build()
     }


Test patch:
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt
index d0255aa4528..44846f80107 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt
@@ -11,12 +11,15 @@ import android.content.Context
 import android.os.Build
 import androidx.core.app.NotificationCompat
 import com.google.common.truth.Truth.assertThat
+import io.element.android.appconfig.NotificationConfig
 import io.element.android.libraries.matrix.api.media.MediaSource
 import io.element.android.libraries.matrix.test.A_ROOM_ID
 import io.element.android.libraries.matrix.test.A_TIMESTAMP
 import io.element.android.libraries.matrix.ui.components.aMatrixUser
 import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
 import io.element.android.libraries.matrix.ui.media.MediaRequestData
+import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE
+import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE
 import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
 import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
 import io.element.android.libraries.push.test.notifications.FakeImageLoader
@@ -156,6 +159,13 @@ class DefaultRoomGroupMessageCreatorTest {
         )
         assertThat(result.number).isEqualTo(2)
         assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10)
+        val actionTitles = result.actions?.map { it.title }
+        assertThat(actionTitles).isEqualTo(
+            listOfNotNull(
+                MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION },
+                QUICK_REPLY_ACTION_TITLE.takeIf { NotificationConfig.SHOW_QUICK_REPLY_ACTION },
+            )
+        )
         assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }
 
@@ -175,7 +185,12 @@ class DefaultRoomGroupMessageCreatorTest {
             imageLoader = fakeImageLoader.getImageLoader(),
             existingNotification = null,
         )
-        assertThat(result.actions).isNull()
+        val actionTitles = result.actions?.map { it.title }
+        assertThat(actionTitles).isEqualTo(
+            listOfNotNull(
+                MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION }
+            )
+        )
         assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }
 
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt
index 9e2f3c10389..9a25ad2a726 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt
@@ -13,6 +13,7 @@ import android.os.Build
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
 import com.google.common.truth.Truth.assertThat
+import io.element.android.appconfig.NotificationConfig
 import io.element.android.libraries.core.meta.BuildMeta
 import io.element.android.libraries.matrix.test.AN_EVENT_ID
 import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -151,6 +152,13 @@ class DefaultNotificationCreatorTest {
         result.commonAssertions(
             expectedCategory = null,
         )
+        val actionTitles = result.actions?.map { it.title }
+        assertThat(actionTitles).isEqualTo(
+            listOfNotNull(
+                REJECT_INVITATION_ACTION_TITLE.takeIf { NotificationConfig.SHOW_ACCEPT_AND_DECLINE_INVITE_ACTIONS },
+                ACCEPT_INVITATION_ACTION_TITLE.takeIf { NotificationConfig.SHOW_ACCEPT_AND_DECLINE_INVITE_ACTIONS },
+            )
+        )
     }
 
     @Test
@@ -271,6 +279,11 @@ class DefaultNotificationCreatorTest {
     }
 }
 
+const val MARK_AS_READ_ACTION_TITLE = "MarkAsReadAction"
+const val QUICK_REPLY_ACTION_TITLE = "QuickReplyAction"
+const val ACCEPT_INVITATION_ACTION_TITLE = "AcceptInvitationAction"
+const val REJECT_INVITATION_ACTION_TITLE = "RejectInvitationAction"
+
 fun createNotificationCreator(
     context: Context = RuntimeEnvironment.getApplication(),
     buildMeta: BuildMeta = aBuildMeta(),
@@ -291,26 +304,26 @@ fun createNotificationCreator(
         markAsReadActionFactory = MarkAsReadActionFactory(
             context = context,
             actionIds = NotificationActionIds(buildMeta),
-            stringProvider = FakeStringProvider("MarkAsReadActionFactory"),
+            stringProvider = FakeStringProvider(MARK_AS_READ_ACTION_TITLE),
             clock = FakeSystemClock(),
         ),
         quickReplyActionFactory = QuickReplyActionFactory(
             context = context,
             actionIds = NotificationActionIds(buildMeta),
-            stringProvider = FakeStringProvider("QuickReplyActionFactory"),
+            stringProvider = FakeStringProvider(QUICK_REPLY_ACTION_TITLE),
             clock = FakeSystemClock(),
         ),
         bitmapLoader = bitmapLoader,
         acceptInvitationActionFactory = AcceptInvitationActionFactory(
             context = context,
             actionIds = NotificationActionIds(buildMeta),
-            stringProvider = FakeStringProvider("AcceptInvitationActionFactory"),
+            stringProvider = FakeStringProvider(ACCEPT_INVITATION_ACTION_TITLE),
             clock = FakeSystemClock(),
         ),
         rejectInvitationActionFactory = RejectInvitationActionFactory(
             context = context,
             actionIds = NotificationActionIds(buildMeta),
-            stringProvider = FakeStringProvider("RejectInvitationActionFactory"),
+            stringProvider = FakeStringProvider(REJECT_INVITATION_ACTION_TITLE),
             clock = FakeSystemClock(),
         ),
     )