Skip to content

Commit

Permalink
Open user profile and room with event from permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
bmarty committed Apr 30, 2024
1 parent c690b38 commit d460711
Show file tree
Hide file tree
Showing 93 changed files with 1,496 additions and 366 deletions.
29 changes: 29 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@

<data android:scheme="io.element" />
</intent-filter>
<!--
Element web links
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
https://developer.android.com/training/app-links#web-links
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" />
<data android:host="app.element.io" />
<data android:host="develop.element.io" />
</intent-filter>
<!--
matrix.to links
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
https://developer.android.com/training/app-links#web-links
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" />
<data android:host="matrix.to" />
</intent-filter>
</activity>

<provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,19 @@ 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
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -375,11 +395,30 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}

suspend fun attachRoom(roomId: RoomId) {
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
if (!canShowRoomList()) return
attachChild<RoomFlowNode> {
backstack.singleTop(NavTarget.RoomList)
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
backstack.push(
NavTarget.Room(
roomIdOrAlias = roomIdOrAlias,
initialElement = RoomNavigationTarget.Messages(
focusedEventId = eventId
)
)
)
}
}

suspend fun attachUser(userId: UserId) {
if (!canShowRoomList()) return
attachChild<Node> {
backstack.singleTop(NavTarget.RoomList)
backstack.push(
NavTarget.UserProfile(
userId = userId,
)
)
}
}

Expand Down
31 changes: 28 additions & 3 deletions appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -279,17 +281,39 @@ 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)
.attachSession()
.apply {
when (deeplinkData) {
is DeeplinkData.Root -> attachRoomList()
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
}
}
}
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -179,6 +182,9 @@ class IntentResolverTest {
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser()
),
permalinkParser = FakePermalinkParser(
result = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
<string name="screen_create_room_room_name_label">"Raum-Name"</string>
<string name="screen_create_room_room_name_label">"Raumname"</string>
<string name="screen_create_room_title">"Raum erstellen"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
Expand Down
3 changes: 1 addition & 2 deletions features/ftue/impl/src/main/res/values-be/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Выберыце %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Паказаць QR-код”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Выконвайце паказаныя інструкцыі"</string>
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
Expand Down
3 changes: 1 addition & 2 deletions features/ftue/impl/src/main/res/values-cs/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Vybrat %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Zobrazit QR kód\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podle uvedených pokynů"</string>
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
Expand Down
3 changes: 1 addition & 2 deletions features/ftue/impl/src/main/res/values-de/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Wähle %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</string>
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
Expand Down
1 change: 0 additions & 1 deletion features/ftue/impl/src/main/res/values-fr/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Choisissez %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Afficher le QR code”"</string>
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
Expand Down
3 changes: 1 addition & 2 deletions features/ftue/impl/src/main/res/values-hu/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Válassza ezt: %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"„QR-kód megjelenítése”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Kövesse a látható utasításokat"</string>
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
Expand Down
1 change: 0 additions & 1 deletion features/ftue/impl/src/main/res/values-in/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Pilih %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Tampilkan kode QR”"</string>
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
Expand Down
Loading

0 comments on commit d460711

Please sign in to comment.