Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8e5bf26
Separate thread notifications into their own notifications when the f…
jmartinesp Oct 23, 2025
ce1e6e3
Add permalink navigation to threads from notifications, focusing on t…
jmartinesp Oct 23, 2025
afc9130
Fix redactions in threads
jmartinesp Oct 23, 2025
a7b2495
Fix and add tests
jmartinesp Oct 23, 2025
9be020b
Clear notifications for a thread when visiting it
jmartinesp Oct 23, 2025
4417ae8
Fix opening a thread happening twice, first because of the `openThrea…
jmartinesp Oct 23, 2025
e6a9321
Make opening a room through a notification also focus on the latest e…
jmartinesp Oct 23, 2025
c812cc1
Add helper `NotificationCreator.messageTag` function
jmartinesp Oct 23, 2025
9382c47
Try using `Icon` instead of `Bitmap` for `largeIcon`
jmartinesp Oct 24, 2025
7bfcdfb
Reorder `createOpenThreadPendingIntent` parameters
jmartinesp Oct 27, 2025
512267d
Remove unused `ROOM_CALL_NOTIFICATION_ID`: `FOREGROUND_SERVICE_NOTIFI…
jmartinesp Oct 27, 2025
2313a04
Reorder parameters in `IntentProvider.getViewRoomIntent`
jmartinesp Oct 27, 2025
86ae942
Remove unnecessary `apply`
jmartinesp Oct 27, 2025
eda5327
Simplify `DefaultDeepLinkCreator`
jmartinesp Oct 27, 2025
85baa0d
Revert removal of OS version check in DefaultNotificationConversation…
jmartinesp Oct 27, 2025
4579ad5
Use `NotificationCreator.messageTag` to get the expected tag in `Defa…
jmartinesp Oct 27, 2025
c76c332
Fix lint issue
jmartinesp Oct 27, 2025
a4c5fb9
Refactor navigation to thread.
jmartinesp Oct 28, 2025
718c3c2
Make sure the main timeline focuses on the thread root id too when na…
jmartinesp Oct 28, 2025
c0dc6bc
Revert incorrect `Assertions` change
jmartinesp Oct 29, 2025
462fde9
Handle "Mark as read" action for thread notification, but disable it …
bmarty Oct 29, 2025
3ab0324
Fix minor issues
jmartinesp Oct 29, 2025
1634f65
Add `advanceUntilIdle` to try to unblock test failing in CI
jmartinesp Oct 30, 2025
6a9ca09
Restore mark as read actions for threads, using `timeline.markAsRead`…
jmartinesp Oct 30, 2025
05aeef8
Fix `NotificationBroadcastReceiverHandlerTest` tests
jmartinesp Oct 30, 2025
405b86f
Log the failure
bmarty Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.deeplink.api.DeepLinkCreator
import io.element.android.libraries.di.annotations.ApplicationContext
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
Expand All @@ -29,10 +30,11 @@ class DefaultIntentProvider(
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
eventId: EventId?,
): Intent {
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
data = deepLinkCreator.create(sessionId, roomId, threadId, eventId).toUri()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import android.content.Context
import android.content.Intent
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.deeplink.api.DeepLinkCreator
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.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
Expand All @@ -31,26 +33,28 @@ import org.robolectric.RuntimeEnvironment
class DefaultIntentProviderTest {
@Test
fun `test getViewRoomIntent with data`() {
val deepLinkCreator = lambdaRecorder<SessionId, RoomId?, ThreadId?, String> { _, _, _ -> "deepLinkCreatorResult" }
val deepLinkCreator = lambdaRecorder<SessionId, RoomId?, ThreadId?, EventId?, String> { _, _, _, _ -> "deepLinkCreatorResult" }
val sut = createDefaultIntentProvider(
deepLinkCreator = { sessionId, roomId, threadId -> deepLinkCreator.invoke(sessionId, roomId, threadId) },
deepLinkCreator = { sessionId, roomId, threadId, eventId -> deepLinkCreator.invoke(sessionId, roomId, threadId, eventId) },
)
val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
eventId = AN_EVENT_ID,
)
result.commonAssertions()
assertThat(result.data.toString()).isEqualTo("deepLinkCreatorResult")
deepLinkCreator.assertions().isCalledOnce().with(
value(A_SESSION_ID),
value(A_ROOM_ID),
value(A_THREAD_ID),
value(AN_EVENT_ID),
)
}

private fun createDefaultIntentProvider(
deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _ -> "" },
deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _, _ -> "" },
): DefaultIntentProvider {
return DefaultIntentProvider(
context = RuntimeEnvironment.getApplication() as Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -496,7 +497,7 @@ class LoggedInFlowNode(
trigger: JoinedRoom.Trigger? = null,
eventId: EventId? = null,
clearBackstack: Boolean,
) {
): RoomFlowNode {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -388,26 +392,44 @@ 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)
}
}
}

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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -79,6 +81,7 @@ class IntentResolverTest {
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
eventId = null,
)
)
)
Expand All @@ -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 {
Expand All @@ -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,
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
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 kotlinx.parcelize.Parcelize

interface MessagesEntryPoint : FeatureEntryPoint {
sealed interface InitialTarget : Parcelable {
@Parcelize
data class Messages(val focusedEventId: EventId?) : InitialTarget
data class Messages(
val focusedEventId: EventId?,
) : InitialTarget

@Parcelize
data object PinnedMessages : InitialTarget
Expand All @@ -46,3 +49,7 @@ interface MessagesEntryPoint : FeatureEntryPoint {

fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
}

interface MessagesEntryPointNode {
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?)
}
1 change: 1 addition & 0 deletions features/messages/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.features.networkmonitor.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
Expand Down
Loading
Loading