Open user profile and room with event from permalink

This commit is contained in:
Benoit Marty 2024-05-01 10:15:54 +02:00 committed by Benoit Marty
parent 682fd45e00
commit 0476bd95c8
93 changed files with 1499 additions and 370 deletions

View file

@ -56,6 +56,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@ -64,9 +65,11 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.di.AppScope
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.MAIN_SPACE
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.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState
@ -91,6 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
private val ftueEntryPoint: FtueEntryPoint,
private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
@ -197,6 +201,11 @@ class LoggedInFlowNode @AssistedInject constructor(
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
) : NavTarget
@Parcelize
data class UserProfile(
val userId: UserId,
) : NavTarget
@Parcelize
data class Settings(
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
@ -270,14 +279,14 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
}
override fun onPermalinkClicked(data: PermalinkData) {
when (data) {
is PermalinkData.UserLink -> {
// FIXME Add a user profile screen.
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
// Should not happen (handled by MessagesNode)
Timber.e("User link clicked: ${data.userId}.")
}
is PermalinkData.RoomLink -> {
backstack.push(
@ -306,6 +315,17 @@ class LoggedInFlowNode @AssistedInject constructor(
)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
userProfileEntryPoint.nodeBuilder(this, buildContext)
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
.callback(callback)
.build()
}
is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback {
override fun onOpenBugReport() {
@ -321,7 +341,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
return preferencesEntryPoint.nodeBuilder(this, buildContext)
preferencesEntryPoint.nodeBuilder(this, buildContext)
.params(inputs)
.callback(callback)
.build()
@ -363,12 +383,32 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
suspend fun attachRoom(roomId: RoomId) {
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
attachChild<RoomFlowNode> {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
backstack.push(
NavTarget.Room(
roomIdOrAlias = roomIdOrAlias,
initialElement = RoomNavigationTarget.Messages(
focusedEventId = eventId
)
)
)
}
}
suspend fun attachUser(userId: UserId) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
attachChild<Node> {
backstack.push(
NavTarget.UserProfile(
userId = userId,
)
)
}
}

View file

@ -55,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.sessionstorage.api.LoggedInState
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
@ -279,9 +281,31 @@ class RootFlowNode @AssistedInject constructor(
when (resolvedIntent) {
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
}
}
private suspend fun navigateTo(permalinkData: PermalinkData) {
Timber.d("Navigating to $permalinkData")
attachSession(null)
.attachSession()
.apply {
when (permalinkData) {
is PermalinkData.FallbackLink -> Unit
is PermalinkData.RoomEmailInviteLink -> Unit
is PermalinkData.RoomLink -> {
attachRoom(
roomIdOrAlias = permalinkData.roomIdOrAlias,
eventId = permalinkData.eventId,
)
}
is PermalinkData.UserLink -> {
attachUser(permalinkData.userId)
}
}
}
}
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
Timber.d("Navigating to $deeplinkData")
attachSession(deeplinkData.sessionId)
@ -289,7 +313,7 @@ class RootFlowNode @AssistedInject constructor(
.apply {
when (deeplinkData) {
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
}
}
}
@ -298,10 +322,11 @@ class RootFlowNode @AssistedInject constructor(
oidcActionFlow.post(oidcAction)
}
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
// [sessionId] will be null for permalink.
private suspend fun attachSession(sessionId: SessionId?): LoggedInAppScopeFlowNode {
// TODO handle multi-session
return waitForChildAttached { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
}
}
}

View file

@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcIntentResolver
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import timber.log.Timber
import javax.inject.Inject
sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
}
class IntentResolver @Inject constructor(
private val deeplinkParser: DeeplinkParser,
private val oidcIntentResolver: OidcIntentResolver
private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser,
) {
fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null
// Coming from a notification?
val deepLinkData = deeplinkParser.getFromIntent(intent)
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
// Coming during login using Oidc?
val oidcAction = oidcIntentResolver.resolve(intent)
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
// External link clicked? (matrix.to, element.io, etc.)
val permalinkData = intent
.takeIf { it.action == Intent.ACTION_VIEW }
?.dataString
?.let { permalinkParser.parse(it) }
?.takeIf { it !is PermalinkData.FallbackLink }
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
// Unknown intent
Timber.w("Unknown intent")
return null

View file

@ -18,6 +18,7 @@ package io.element.android.appnav.intent
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.oidc.OidcAction
@ -26,9 +27,11 @@ import io.element.android.features.login.impl.oidc.OidcUrlParser
import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
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
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
@ -179,6 +182,9 @@ class IntentResolverTest {
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser()
),
permalinkParser = FakePermalinkParser(
result = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
),
)
}
}