Split notifications for messages in threads (#5595)
* Separate thread notifications into their own notifications when the feature flag is enabled. Otherwise, set the `threadId` to null so it'll behave as usual. It's done this way to avoid having to inject `FeatureFlagService` in several places. * Add permalink navigation to threads from notifications, focusing on the latest event in the list of messages of the notification tapped * Fix redactions in threads * Clear notifications for a thread when visiting it * Fix opening a thread happening twice, first because of the `openThreadId` value, then because of the `focusedEventId` one * Make opening a room through a notification also focus on the latest event * Add helper `NotificationCreator.messageTag` function * Remove unused `ROOM_CALL_NOTIFICATION_ID`: `FOREGROUND_SERVICE_NOTIFICATION_ID`+ `ForegroundServiceType` is used instead * Simplify `DefaultDeepLinkCreator` * Make sure the main timeline focuses on the thread root id too when navigating to a thread * Handle "Mark as read" action for thread notification, using `timeline.markAsRead` * Log failures to mark rooms as read using the notification action --------- Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
parent
e8b7db22cd
commit
7facc40771
55 changed files with 702 additions and 284 deletions
|
|
@ -68,6 +68,7 @@ import io.element.android.features.verifysession.api.IncomingVerificationEntryPo
|
|||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.architecture.waitForNavTargetAttached
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
|
|
@ -496,7 +497,7 @@ class LoggedInFlowNode(
|
|||
trigger: JoinedRoom.Trigger? = null,
|
||||
eventId: EventId? = null,
|
||||
clearBackstack: Boolean,
|
||||
) {
|
||||
): RoomFlowNode {
|
||||
waitForNavTargetAttached { navTarget ->
|
||||
navTarget is NavTarget.Home
|
||||
}
|
||||
|
|
@ -509,6 +510,15 @@ class LoggedInFlowNode(
|
|||
)
|
||||
backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack))
|
||||
}
|
||||
|
||||
// If we don't do this check, we might be returning while a previous node with the same type is still displayed
|
||||
// This means we may attach some new nodes to that one, which will be quickly replaced by the one instantiated above
|
||||
return waitForChildAttached<RoomFlowNode, NavTarget> {
|
||||
it is NavTarget.Room &&
|
||||
it.roomIdOrAlias == roomIdOrAlias &&
|
||||
it.initialElement is RoomNavigationTarget.Root &&
|
||||
it.initialElement.eventId == eventId
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun attachUser(userId: UserId) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import io.element.android.annotations.ContributesNode
|
|||
import io.element.android.appnav.di.MatrixSessionCache
|
||||
import io.element.android.appnav.intent.IntentResolver
|
||||
import io.element.android.appnav.intent.ResolvedIntent
|
||||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||
import io.element.android.appnav.root.RootPresenter
|
||||
import io.element.android.appnav.root.RootView
|
||||
|
|
@ -49,7 +50,10 @@ import io.element.android.libraries.core.uri.ensureProtocol
|
|||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
|
|
@ -388,13 +392,19 @@ class RootFlowNode(
|
|||
is PermalinkData.FallbackLink -> Unit
|
||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||
is PermalinkData.RoomLink -> {
|
||||
// If there is a thread id, focus on it in the main timeline
|
||||
val focusedEventId = if (permalinkData.threadId != null) {
|
||||
permalinkData.threadId?.asEventId()
|
||||
} else {
|
||||
permalinkData.eventId
|
||||
}
|
||||
attachRoom(
|
||||
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||
serverNames = permalinkData.viaParameters,
|
||||
eventId = permalinkData.eventId,
|
||||
eventId = focusedEventId,
|
||||
clearBackstack = true
|
||||
)
|
||||
).maybeAttachThread(permalinkData.threadId, permalinkData.eventId)
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
attachUser(permalinkData.userId)
|
||||
|
|
@ -402,12 +412,24 @@ class RootFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun RoomFlowNode.maybeAttachThread(threadId: ThreadId?, focusedEventId: EventId?) {
|
||||
if (threadId != null) {
|
||||
attachThread(threadId, focusedEventId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||
Timber.d("Navigating to $deeplinkData")
|
||||
attachSession(deeplinkData.sessionId).apply {
|
||||
attachSession(deeplinkData.sessionId).let { loggedInFlowNode ->
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
||||
is DeeplinkData.Room -> {
|
||||
loggedInFlowNode.attachRoom(
|
||||
roomIdOrAlias = deeplinkData.roomId.toRoomIdOrAlias(),
|
||||
eventId = if (deeplinkData.threadId != null) deeplinkData.threadId?.asEventId() else deeplinkData.eventId,
|
||||
clearBackstack = true,
|
||||
).maybeAttachThread(deeplinkData.threadId, deeplinkData.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,9 +39,11 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.core.coroutine.withPreviousValue
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
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.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
|
|
@ -211,6 +213,11 @@ class RoomFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
|
||||
waitForChildAttached<JoinedRoomFlowNode>()
|
||||
.attachThread(threadId, focusedEventId)
|
||||
}
|
||||
|
||||
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
|
||||
LoadingRoomNodeView(
|
||||
state = LoadingRoomState.Loading,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import kotlinx.parcelize.Parcelize
|
|||
|
||||
sealed interface RoomNavigationTarget : Parcelable {
|
||||
@Parcelize
|
||||
data class Root(val eventId: EventId? = null) : RoomNavigationTarget
|
||||
data class Root(
|
||||
val eventId: EventId? = null,
|
||||
) : RoomNavigationTarget
|
||||
|
||||
@Parcelize
|
||||
data object Details : RoomNavigationTarget
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ import io.element.android.libraries.architecture.NodeInputs
|
|||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
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.ThreadId
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -121,6 +123,11 @@ class JoinedRoomFlowNode(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
|
||||
waitForChildAttached<JoinedRoomLoadedFlowNode>()
|
||||
.attachThread(threadId, focusedEventId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView(
|
||||
|
|
|
|||
|
|
@ -25,18 +25,21 @@ import io.element.android.appnav.di.RoomGraphFactory
|
|||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPointNode
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
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.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
|
@ -240,7 +243,9 @@ class JoinedRoomLoadedFlowNode(
|
|||
data object Space : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Messages(val focusedEventId: EventId? = null) : NavTarget
|
||||
data class Messages(
|
||||
val focusedEventId: EventId? = null,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomDetails : NavTarget
|
||||
|
|
@ -258,6 +263,13 @@ class JoinedRoomLoadedFlowNode(
|
|||
data object RoomNotificationSettings : NavTarget
|
||||
}
|
||||
|
||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
|
||||
val messageNode = waitForChildAttached<Node, NavTarget> { navTarget ->
|
||||
navTarget is NavTarget.Messages
|
||||
}
|
||||
(messageNode as? MessagesEntryPointNode)?.attachThread(threadId, focusedEventId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io.element.android.features.login.test.FakeLoginIntentResolver
|
|||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
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
|
||||
|
|
@ -67,6 +68,7 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
|
|
@ -79,6 +81,7 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -91,6 +94,7 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
|
|
@ -103,6 +107,59 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve navigation intent event`() {
|
||||
val sut = createIntentResolver(
|
||||
deeplinkParserResult = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Navigation(
|
||||
deeplinkData = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve navigation intent thread and event`() {
|
||||
val sut = createIntentResolver(
|
||||
deeplinkParserResult = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Navigation(
|
||||
deeplinkData = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue