Skip to content

Commit e9c262e

Browse files
authored
Merge pull request #2776 from element-hq/feature/bma/externalLinks
Open user profile and room with event from permalink
2 parents 682fd45 + baf3877 commit e9c262e

File tree

295 files changed

+1793
-425
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

295 files changed

+1793
-425
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,45 @@
7474

7575
<data android:scheme="io.element" />
7676
</intent-filter>
77+
<!--
78+
Element web links
79+
-->
80+
<intent-filter android:autoVerify="true">
81+
<action android:name="android.intent.action.VIEW" />
82+
83+
<category android:name="android.intent.category.DEFAULT" />
84+
<category android:name="android.intent.category.BROWSABLE" />
85+
86+
<data android:scheme="https" />
87+
<data android:host="*.element.io" />
88+
</intent-filter>
89+
<!--
90+
matrix.to links
91+
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
92+
https://developer.android.com/training/app-links#web-links
93+
-->
94+
<intent-filter>
95+
<action android:name="android.intent.action.VIEW" />
96+
97+
<category android:name="android.intent.category.DEFAULT" />
98+
<category android:name="android.intent.category.BROWSABLE" />
99+
100+
<data android:scheme="https" />
101+
<data android:host="matrix.to" />
102+
</intent-filter>
103+
<!--
104+
links from matrix.to website
105+
-->
106+
<intent-filter>
107+
<action android:name="android.intent.action.VIEW" />
108+
109+
<category android:name="android.intent.category.DEFAULT" />
110+
<category android:name="android.intent.category.BROWSABLE" />
111+
112+
<data android:scheme="element" />
113+
<data android:host="user" />
114+
<data android:host="room" />
115+
</intent-filter>
77116
</activity>
78117

79118
<provider

app/src/main/kotlin/io/element/android/x/MainActivity.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import io.element.android.compound.theme.ElementTheme
3939
import io.element.android.compound.theme.Theme
4040
import io.element.android.compound.theme.isDark
4141
import io.element.android.compound.theme.mapToTheme
42+
import io.element.android.features.call.ui.ElementCallActivity
4243
import io.element.android.features.lockscreen.api.handleSecureFlag
4344
import io.element.android.features.lockscreen.api.isLocked
4445
import io.element.android.libraries.architecture.bindings
@@ -58,6 +59,13 @@ class MainActivity : NodeActivity() {
5859
Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}")
5960
installSplashScreen()
6061
super.onCreate(savedInstanceState)
62+
if (ElementCallActivity.maybeStart(this, intent)) {
63+
Timber.tag(loggerTag.value).w("Starting Element Call Activity")
64+
if (savedInstanceState == null) {
65+
finish()
66+
return
67+
}
68+
}
6169
appBindings = bindings()
6270
appBindings.lockScreenService().handleSecureFlag(this)
6371
enableEdgeToEdge()
@@ -135,11 +143,18 @@ class MainActivity : NodeActivity() {
135143
* Called when:
136144
* - the launcher icon is clicked (if the app is already running);
137145
* - a notification is clicked.
146+
* - a deep link have been clicked
138147
* - the app is going to background (<- this is strange)
139148
*/
140149
override fun onNewIntent(intent: Intent) {
141150
super.onNewIntent(intent)
142151
Timber.tag(loggerTag.value).w("onNewIntent")
152+
153+
if (ElementCallActivity.maybeStart(this, intent)) {
154+
Timber.tag(loggerTag.value).w("Starting Element Call Activity")
155+
return
156+
}
157+
143158
// If the mainNode is not init yet, keep the intent for later.
144159
// It can happen when the activity is killed by the system. The methods are called in this order :
145160
// onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit

appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
5656
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
5757
import io.element.android.features.roomlist.api.RoomListEntryPoint
5858
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
59+
import io.element.android.features.userprofile.api.UserProfileEntryPoint
5960
import io.element.android.libraries.architecture.BackstackView
6061
import io.element.android.libraries.architecture.BaseFlowNode
6162
import io.element.android.libraries.architecture.createNode
@@ -64,9 +65,11 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
6465
import io.element.android.libraries.di.AppScope
6566
import io.element.android.libraries.di.SessionScope
6667
import io.element.android.libraries.matrix.api.MatrixClient
68+
import io.element.android.libraries.matrix.api.core.EventId
6769
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
6870
import io.element.android.libraries.matrix.api.core.RoomId
6971
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
72+
import io.element.android.libraries.matrix.api.core.UserId
7073
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
7174
import io.element.android.libraries.matrix.api.permalink.PermalinkData
7275
import io.element.android.libraries.matrix.api.sync.SyncState
@@ -91,6 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor(
9194
private val createRoomEntryPoint: CreateRoomEntryPoint,
9295
private val appNavigationStateService: AppNavigationStateService,
9396
private val secureBackupEntryPoint: SecureBackupEntryPoint,
97+
private val userProfileEntryPoint: UserProfileEntryPoint,
9498
private val ftueEntryPoint: FtueEntryPoint,
9599
private val coroutineScope: CoroutineScope,
96100
private val networkMonitor: NetworkMonitor,
@@ -197,6 +201,11 @@ class LoggedInFlowNode @AssistedInject constructor(
197201
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
198202
) : NavTarget
199203

204+
@Parcelize
205+
data class UserProfile(
206+
val userId: UserId,
207+
) : NavTarget
208+
200209
@Parcelize
201210
data class Settings(
202211
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
@@ -270,14 +279,14 @@ class LoggedInFlowNode @AssistedInject constructor(
270279
}
271280

272281
override fun onForwardedToSingleRoom(roomId: RoomId) {
273-
coroutineScope.launch { attachRoom(roomId) }
282+
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
274283
}
275284

276285
override fun onPermalinkClicked(data: PermalinkData) {
277286
when (data) {
278287
is PermalinkData.UserLink -> {
279-
// FIXME Add a user profile screen.
280-
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
288+
// Should not happen (handled by MessagesNode)
289+
Timber.e("User link clicked: ${data.userId}.")
281290
}
282291
is PermalinkData.RoomLink -> {
283292
backstack.push(
@@ -306,6 +315,17 @@ class LoggedInFlowNode @AssistedInject constructor(
306315
)
307316
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
308317
}
318+
is NavTarget.UserProfile -> {
319+
val callback = object : UserProfileEntryPoint.Callback {
320+
override fun onOpenRoom(roomId: RoomId) {
321+
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
322+
}
323+
}
324+
userProfileEntryPoint.nodeBuilder(this, buildContext)
325+
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
326+
.callback(callback)
327+
.build()
328+
}
309329
is NavTarget.Settings -> {
310330
val callback = object : PreferencesEntryPoint.Callback {
311331
override fun onOpenBugReport() {
@@ -321,7 +341,7 @@ class LoggedInFlowNode @AssistedInject constructor(
321341
}
322342
}
323343
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
324-
return preferencesEntryPoint.nodeBuilder(this, buildContext)
344+
preferencesEntryPoint.nodeBuilder(this, buildContext)
325345
.params(inputs)
326346
.callback(callback)
327347
.build()
@@ -363,12 +383,32 @@ class LoggedInFlowNode @AssistedInject constructor(
363383
}
364384
}
365385

366-
suspend fun attachRoom(roomId: RoomId) {
386+
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
367387
waitForNavTargetAttached { navTarget ->
368388
navTarget is NavTarget.RoomList
369389
}
370390
attachChild<RoomFlowNode> {
371-
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
391+
backstack.push(
392+
NavTarget.Room(
393+
roomIdOrAlias = roomIdOrAlias,
394+
initialElement = RoomNavigationTarget.Messages(
395+
focusedEventId = eventId
396+
)
397+
)
398+
)
399+
}
400+
}
401+
402+
suspend fun attachUser(userId: UserId) {
403+
waitForNavTargetAttached { navTarget ->
404+
navTarget is NavTarget.RoomList
405+
}
406+
attachChild<Node> {
407+
backstack.push(
408+
NavTarget.UserProfile(
409+
userId = userId,
410+
)
411+
)
372412
}
373413
}
374414

appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
5555
import io.element.android.libraries.di.AppScope
5656
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
5757
import io.element.android.libraries.matrix.api.core.SessionId
58+
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
59+
import io.element.android.libraries.matrix.api.permalink.PermalinkData
5860
import io.element.android.libraries.sessionstorage.api.LoggedInState
5961
import kotlinx.coroutines.flow.distinctUntilChanged
6062
import kotlinx.coroutines.flow.launchIn
@@ -279,17 +281,37 @@ class RootFlowNode @AssistedInject constructor(
279281
when (resolvedIntent) {
280282
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
281283
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
284+
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
282285
}
283286
}
284287

288+
private suspend fun navigateTo(permalinkData: PermalinkData) {
289+
Timber.d("Navigating to $permalinkData")
290+
attachSession(null)
291+
.apply {
292+
when (permalinkData) {
293+
is PermalinkData.FallbackLink -> Unit
294+
is PermalinkData.RoomEmailInviteLink -> Unit
295+
is PermalinkData.RoomLink -> {
296+
attachRoom(
297+
roomIdOrAlias = permalinkData.roomIdOrAlias,
298+
eventId = permalinkData.eventId,
299+
)
300+
}
301+
is PermalinkData.UserLink -> {
302+
attachUser(permalinkData.userId)
303+
}
304+
}
305+
}
306+
}
307+
285308
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
286309
Timber.d("Navigating to $deeplinkData")
287310
attachSession(deeplinkData.sessionId)
288-
.attachSession()
289311
.apply {
290312
when (deeplinkData) {
291313
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
292-
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
314+
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
293315
}
294316
}
295317
}
@@ -298,10 +320,12 @@ class RootFlowNode @AssistedInject constructor(
298320
oidcActionFlow.post(oidcAction)
299321
}
300322

301-
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
323+
// [sessionId] will be null for permalink.
324+
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
302325
// TODO handle multi-session
303-
return waitForChildAttached { navTarget ->
304-
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
326+
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
327+
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
305328
}
329+
.attachSession()
306330
}
307331
}

appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction
2121
import io.element.android.features.login.api.oidc.OidcIntentResolver
2222
import io.element.android.libraries.deeplink.DeeplinkData
2323
import io.element.android.libraries.deeplink.DeeplinkParser
24+
import io.element.android.libraries.matrix.api.permalink.PermalinkData
25+
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
2426
import timber.log.Timber
2527
import javax.inject.Inject
2628

2729
sealed interface ResolvedIntent {
2830
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
2931
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
32+
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
3033
}
3134

3235
class IntentResolver @Inject constructor(
3336
private val deeplinkParser: DeeplinkParser,
34-
private val oidcIntentResolver: OidcIntentResolver
37+
private val oidcIntentResolver: OidcIntentResolver,
38+
private val permalinkParser: PermalinkParser,
3539
) {
3640
fun resolve(intent: Intent): ResolvedIntent? {
3741
if (intent.canBeIgnored()) return null
3842

43+
// Coming from a notification?
3944
val deepLinkData = deeplinkParser.getFromIntent(intent)
4045
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
4146

47+
// Coming during login using Oidc?
4248
val oidcAction = oidcIntentResolver.resolve(intent)
4349
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
4450

51+
// External link clicked? (matrix.to, element.io, etc.)
52+
val permalinkData = intent
53+
.takeIf { it.action == Intent.ACTION_VIEW }
54+
?.dataString
55+
?.let { permalinkParser.parse(it) }
56+
?.takeIf { it !is PermalinkData.FallbackLink }
57+
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
58+
4559
// Unknown intent
4660
Timber.w("Unknown intent")
4761
return null

0 commit comments

Comments
 (0)