feat: Implement deep linking functionality
This commit introduces deep linking capabilities to the application. - Added `DeepLinkActivity` to handle incoming deep links and navigate users to the appropriate content within the app. - Created `DeepLinkUtils` to provide utility functions for parsing and creating deep link URIs. - Updated `MainActivity` to receive and process deep link data from `DeepLinkActivity`. - Modified `ChatRouterScreen` and its `ViewModel` to handle navigation based on deep link parameters. - Updated `AndroidManifest.xml` to include intent filters for `DeepLinkActivity` to capture relevant URL schemes and paths. - Added `assetlinks.json` and `README_APP_LINKS.md` to support Android App Links verification. - Changed the `applicationId` in `app/build.gradle.kts` from "chat.revolt" to "chat.peptide". - Renamed project in `.idea/.name` from "PEP" to "Revolt". - Changed the default route in `ChatRouterScreen` from `Settings` to `Home`.
This commit is contained in:
parent
b89b1e9415
commit
9d53be9e41
|
|
@ -1 +1 @@
|
||||||
PEP
|
Revolt
|
||||||
|
|
@ -77,7 +77,7 @@ android {
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "chat.revolt"
|
applicationId = "chat.peptide"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = Integer.parseInt("001_003_106".replace("_", ""), 10)
|
versionCode = Integer.parseInt("001_003_106".replace("_", ""), 10)
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,34 @@
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<!-- DeepLink Activity for handling App Links -->
|
||||||
|
<activity
|
||||||
|
android:name=".activities.DeepLinkActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.Revolt">
|
||||||
|
<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:scheme="http" />
|
||||||
|
<data android:host="peptide.chat" />
|
||||||
|
<data android:host="app.peptide.chat" />
|
||||||
|
|
||||||
|
<!-- Channel deep links -->
|
||||||
|
<data android:pathPrefix="/channels/" />
|
||||||
|
<!-- Server deep links -->
|
||||||
|
<data android:pathPrefix="/servers/" />
|
||||||
|
<data android:pathPrefix="/server/" />
|
||||||
|
<!-- User deep links -->
|
||||||
|
<data android:pathPrefix="/users/" />
|
||||||
|
<!-- Message deep links -->
|
||||||
|
<data android:pathPrefix="/messages/" />
|
||||||
|
<!-- Server channel message deep links -->
|
||||||
|
<data android:pathPattern="/server/.*/channel/.*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Android App Links Setup Instructions
|
||||||
|
|
||||||
|
This document provides instructions for setting up Android App Links for the Revolt app.
|
||||||
|
|
||||||
|
## What are Android App Links?
|
||||||
|
|
||||||
|
Android App Links are HTTP URLs that bring users directly to specific content in your Android app. When a user clicks an App Link, the app opens immediately if it's installed, without showing the app chooser dialog.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. Your app must have an intent filter that handles the specific URLs
|
||||||
|
2. Your website must have a Digital Asset Links JSON file that verifies the association with your app
|
||||||
|
|
||||||
|
## Steps to Complete the Setup
|
||||||
|
|
||||||
|
### 1. Get your app's signing certificate fingerprint
|
||||||
|
|
||||||
|
Run the following command to get your app's signing certificate fingerprint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -list -v -keystore your_keystore.jks -alias your_alias
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for the SHA-256 fingerprint in the output. Remove all colons (:) from the fingerprint.
|
||||||
|
|
||||||
|
### 2. Update the assetlinks.json file
|
||||||
|
|
||||||
|
Open the `assetlinks.json` file in this directory and replace the placeholder with your actual SHA-256 fingerprint:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "chat.revolt",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"YOUR_SHA256_FINGERPRINT_HERE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Host the assetlinks.json file on your server
|
||||||
|
|
||||||
|
Upload the `assetlinks.json` file to the following location on your server:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://revolt.chat/.well-known/assetlinks.json
|
||||||
|
https://app.revolt.chat/.well-known/assetlinks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure the file is accessible via HTTPS and returns with Content-Type: application/json.
|
||||||
|
|
||||||
|
### 4. Test your App Links
|
||||||
|
|
||||||
|
Use the Android Debug Bridge (ADB) to test your App Links:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb shell am start -a android.intent.action.VIEW -d "https://revolt.chat/channels/CHANNEL_ID" chat.revolt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If your App Links aren't working:
|
||||||
|
|
||||||
|
1. Verify that the assetlinks.json file is accessible via HTTPS
|
||||||
|
2. Check that the SHA-256 fingerprint in the assetlinks.json file matches your app's signing certificate
|
||||||
|
3. Make sure your app is using the correct signing certificate
|
||||||
|
4. Use the App Links Assistant in Android Studio to verify your App Links setup
|
||||||
|
|
||||||
|
For more information, see the [Android App Links documentation](https://developer.android.com/training/app-links).
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
[{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "chat.peptide",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"PUT_YOUR_APP_SIGNING_CERTIFICATE_SHA256_FINGERPRINT_HERE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
package chat.revolt.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import chat.revolt.persistence.KVStorage
|
||||||
|
import chat.revolt.utils.DeepLinkUtils
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class DeepLinkActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var kvStorage: KVStorage
|
||||||
|
|
||||||
|
private val viewModel by viewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DeepLinkActivity"
|
||||||
|
|
||||||
|
// Constants for deep link paths
|
||||||
|
const val PATH_CHANNEL = "channel"
|
||||||
|
const val PATH_SERVER = "server"
|
||||||
|
const val PATH_USER = "user"
|
||||||
|
const val PATH_MESSAGE = "message"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Handle the incoming intent
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
|
// Handle the new intent
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIntent(intent: Intent) {
|
||||||
|
val appLinkAction = intent.action
|
||||||
|
val appLinkData: Uri? = intent.data
|
||||||
|
|
||||||
|
if (Intent.ACTION_VIEW == appLinkAction && appLinkData != null) {
|
||||||
|
Log.d(TAG, "Deep link received: $appLinkData")
|
||||||
|
|
||||||
|
// Check if this is a valid Revolt deep link
|
||||||
|
if (!DeepLinkUtils.isRevoltDeepLink(appLinkData)) {
|
||||||
|
Log.d(TAG, "Not a valid Revolt deep link: $appLinkData")
|
||||||
|
startMainActivity()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is logged in and handle deep link in a coroutine
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val token = kvStorage.get("sessionToken")
|
||||||
|
if (token == null) {
|
||||||
|
// User is not logged in, redirect to login screen
|
||||||
|
Log.d(TAG, "User not logged in, redirecting to login screen")
|
||||||
|
startMainActivity("choose-platform")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, check for server/channel/message format
|
||||||
|
val serverChannelMessage = DeepLinkUtils.extractServerChannelMessage(appLinkData)
|
||||||
|
if (serverChannelMessage != null) {
|
||||||
|
Log.d(TAG, "Detected server/channel/message format: ${serverChannelMessage.serverId}/${serverChannelMessage.channelId}/${serverChannelMessage.messageId}")
|
||||||
|
|
||||||
|
if (serverChannelMessage.messageId != null) {
|
||||||
|
// Navigate to the specific message in the channel
|
||||||
|
navigateToMessage(serverChannelMessage.messageId, serverChannelMessage.channelId)
|
||||||
|
} else {
|
||||||
|
// Navigate to the channel in the server
|
||||||
|
navigateToChannel(serverChannelMessage.channelId)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract IDs from the URI using our utility class for other formats
|
||||||
|
val channelId = DeepLinkUtils.extractChannelId(appLinkData)
|
||||||
|
val serverId = DeepLinkUtils.extractServerId(appLinkData)
|
||||||
|
val userId = DeepLinkUtils.extractUserId(appLinkData)
|
||||||
|
val messageId = DeepLinkUtils.extractMessageId(appLinkData)
|
||||||
|
|
||||||
|
when {
|
||||||
|
channelId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to channel: $channelId")
|
||||||
|
navigateToChannel(channelId)
|
||||||
|
}
|
||||||
|
serverId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to server: $serverId")
|
||||||
|
navigateToServer(serverId)
|
||||||
|
}
|
||||||
|
userId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to user: $userId")
|
||||||
|
navigateToUser(userId)
|
||||||
|
}
|
||||||
|
messageId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to message: $messageId")
|
||||||
|
val channelParam = appLinkData.getQueryParameter("channel")
|
||||||
|
navigateToMessage(messageId, channelParam)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Default case, just open the main activity
|
||||||
|
Log.d(TAG, "No specific path, opening main activity")
|
||||||
|
startMainActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a deep link, just open the main activity
|
||||||
|
Log.d(TAG, "Not a deep link, opening main activity")
|
||||||
|
startMainActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToChannel(channelId: String) {
|
||||||
|
Log.d(TAG, "Navigating to channel: $channelId")
|
||||||
|
|
||||||
|
// Use our utility class to create the intent
|
||||||
|
val intent = DeepLinkUtils.createChannelIntent(this, channelId)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToServer(serverId: String) {
|
||||||
|
Log.d(TAG, "Navigating to server: $serverId")
|
||||||
|
|
||||||
|
// Use our utility class to create the intent
|
||||||
|
val intent = DeepLinkUtils.createServerIntent(this, serverId)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToUser(userId: String) {
|
||||||
|
Log.d(TAG, "Navigating to user: $userId")
|
||||||
|
|
||||||
|
// Use our utility class to create the intent
|
||||||
|
val intent = DeepLinkUtils.createUserIntent(this, userId)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMessage(messageId: String, channelId: String?) {
|
||||||
|
Log.d(TAG, "Navigating to message: $messageId in channel: $channelId")
|
||||||
|
|
||||||
|
// Use our utility class to create the intent
|
||||||
|
val intent = DeepLinkUtils.createMessageIntent(this, messageId, channelId)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMainActivity(destination: String? = null) {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
if (destination != null) {
|
||||||
|
intent.putExtra("next_destination", destination)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package chat.revolt.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -127,6 +128,7 @@ import chat.revolt.screens.settings.channel.ChannelSettingsHome
|
||||||
import chat.revolt.screens.settings.channel.ChannelSettingsOverview
|
import chat.revolt.screens.settings.channel.ChannelSettingsOverview
|
||||||
import chat.revolt.screens.settings.channel.ChannelSettingsPermissions
|
import chat.revolt.screens.settings.channel.ChannelSettingsPermissions
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
|
import chat.revolt.utils.DeepLinkHandler
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -383,6 +385,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
RevoltAPI.hydrateFromPersistentCache()
|
RevoltAPI.hydrateFromPersistentCache()
|
||||||
|
|
||||||
|
// Handle deep link data if available
|
||||||
|
handleDeepLinkData(intent)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val windowSizeClass = calculateWindowSizeClass(this)
|
val windowSizeClass = calculateWindowSizeClass(this)
|
||||||
|
|
@ -419,6 +424,70 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
handleDeepLinkData(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeepLinkData(intent: Intent) {
|
||||||
|
// Check for deep link data from DeepLinkActivity
|
||||||
|
val channelId = intent.getStringExtra(DeepLinkActivity.PATH_CHANNEL)
|
||||||
|
val serverId = intent.getStringExtra(DeepLinkActivity.PATH_SERVER)
|
||||||
|
val userId = intent.getStringExtra(DeepLinkActivity.PATH_USER)
|
||||||
|
val messageId = intent.getStringExtra(DeepLinkActivity.PATH_MESSAGE)
|
||||||
|
val nextDestination = intent.getStringExtra("next_destination")
|
||||||
|
|
||||||
|
if (nextDestination != null) {
|
||||||
|
Log.d(TAG, "Setting next destination from deep link: $nextDestination")
|
||||||
|
viewModel.updateNextDestination(nextDestination)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store deep link data in a static companion object for ChatRouterScreen to access
|
||||||
|
DeepLinkHandler.channelId = channelId
|
||||||
|
DeepLinkHandler.serverId = serverId
|
||||||
|
DeepLinkHandler.userId = userId
|
||||||
|
DeepLinkHandler.messageId = messageId
|
||||||
|
DeepLinkHandler.hasDeepLink = channelId != null || serverId != null || userId != null || messageId != null
|
||||||
|
|
||||||
|
// Wait for the app to be ready before navigating to the deep link destination
|
||||||
|
viewModel.viewModelScope.launch {
|
||||||
|
viewModel.isReady.collect { isReady ->
|
||||||
|
if (isReady) {
|
||||||
|
when {
|
||||||
|
channelId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to channel from deep link: $channelId")
|
||||||
|
// Check if Polar mode is enabled
|
||||||
|
if (Experiments.usePolar.isEnabled) {
|
||||||
|
viewModel.updateNextDestination("main/conversation/$channelId")
|
||||||
|
} else {
|
||||||
|
// For non-Polar mode, we'll use the DeepLinkHandler to communicate with ChatRouterScreen
|
||||||
|
viewModel.updateNextDestination("chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to server from deep link: $serverId")
|
||||||
|
// Navigate to chat screen, the ChatRouterScreen will handle the server navigation
|
||||||
|
viewModel.updateNextDestination("chat")
|
||||||
|
}
|
||||||
|
userId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to user from deep link: $userId")
|
||||||
|
// Navigate to chat screen, the ChatRouterScreen will handle the user navigation
|
||||||
|
viewModel.updateNextDestination("chat")
|
||||||
|
}
|
||||||
|
messageId != null -> {
|
||||||
|
Log.d(TAG, "Navigating to message from deep link: $messageId")
|
||||||
|
// Navigate to chat screen, the ChatRouterScreen will handle the message navigation
|
||||||
|
viewModel.updateNextDestination("chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only collect once
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onProvideKeyboardShortcuts(
|
override fun onProvideKeyboardShortcuts(
|
||||||
data: MutableList<KeyboardShortcutGroup>?,
|
data: MutableList<KeyboardShortcutGroup>?,
|
||||||
menu: Menu?,
|
menu: Menu?,
|
||||||
|
|
@ -444,6 +513,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "MainActivity"
|
||||||
init {
|
init {
|
||||||
NativeLibraries.init()
|
NativeLibraries.init()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ import chat.revolt.composables.chat.DisconnectedNotice
|
||||||
import chat.revolt.composables.screens.chat.drawer.ChannelSideDrawer
|
import chat.revolt.composables.screens.chat.drawer.ChannelSideDrawer
|
||||||
import chat.revolt.dialogs.NotificationRationaleDialog
|
import chat.revolt.dialogs.NotificationRationaleDialog
|
||||||
import chat.revolt.internals.Changelogs
|
import chat.revolt.internals.Changelogs
|
||||||
|
import chat.revolt.utils.DeepLinkHandler
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
|
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
|
||||||
import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog
|
import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog
|
||||||
|
|
@ -112,12 +113,12 @@ sealed class ChatRouterDestination {
|
||||||
data class NoCurrentChannel(val serverId: String?) : ChatRouterDestination()
|
data class NoCurrentChannel(val serverId: String?) : ChatRouterDestination()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val default = Settings
|
val default = Home // Changed default from Settings to Home
|
||||||
|
|
||||||
fun fromString(destination: String): ChatRouterDestination {
|
fun fromString(destination: String): ChatRouterDestination {
|
||||||
return when {
|
return when {
|
||||||
destination == "home" -> Settings // previous name for overview
|
destination == "home" -> Home // Changed from Settings to Home
|
||||||
destination == "overview" -> Settings
|
destination == "overview" -> Home // Changed from Settings to Home
|
||||||
destination == "friends" -> Friends
|
destination == "friends" -> Friends
|
||||||
destination.startsWith("no_current_channel/") -> NoCurrentChannel(
|
destination.startsWith("no_current_channel/") -> NoCurrentChannel(
|
||||||
destination.removePrefix(
|
destination.removePrefix(
|
||||||
|
|
@ -150,8 +151,15 @@ class ChatRouterViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val current = kvStorage.get("currentDestination")
|
// Check if there's a deep link to handle first
|
||||||
setSaveDestination(ChatRouterDestination.fromString(current ?: ""))
|
if (DeepLinkHandler.hasDeepLink) {
|
||||||
|
Log.d("ChatRouterViewModel", "Deep link detected in init, skipping saved destination")
|
||||||
|
// Don't set any destination here, let the LaunchedEffect in ChatRouterScreen handle it
|
||||||
|
} else {
|
||||||
|
// No deep link, load the saved destination
|
||||||
|
val current = kvStorage.get("currentDestination")
|
||||||
|
setSaveDestination(ChatRouterDestination.fromString(current ?: ""))
|
||||||
|
}
|
||||||
|
|
||||||
latestChangelogRead = changelogs.hasSeenCurrent()
|
latestChangelogRead = changelogs.hasSeenCurrent()
|
||||||
latestChangelog = changelogs.getLatestChangelogCode()
|
latestChangelog = changelogs.getLatestChangelogCode()
|
||||||
|
|
@ -361,6 +369,63 @@ fun ChatRouterScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle deep links when the ChatRouterScreen is first composed
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// Check if there's a deep link to handle
|
||||||
|
if (DeepLinkHandler.hasDeepLink) {
|
||||||
|
Log.d("ChatRouterScreen", "Processing deep link data")
|
||||||
|
|
||||||
|
// Process the deep link based on priority
|
||||||
|
when {
|
||||||
|
// Priority 1: Handle server/channel/message format (most specific)
|
||||||
|
DeepLinkHandler.messageId != null && DeepLinkHandler.channelId != null -> {
|
||||||
|
val messageId = DeepLinkHandler.messageId!!
|
||||||
|
val channelId = DeepLinkHandler.channelId!!
|
||||||
|
Log.d("ChatRouterScreen", "Setting destination to message: $messageId in channel: $channelId")
|
||||||
|
|
||||||
|
val resolvedChannel = RevoltAPI.channelCache[channelId]
|
||||||
|
if (resolvedChannel == null) {
|
||||||
|
showChannelUnavailableAlert = true
|
||||||
|
} else {
|
||||||
|
// Navigate to the channel first, then the message will be handled by the channel screen
|
||||||
|
viewModel.setSaveDestination(ChatRouterDestination.Channel(channelId))
|
||||||
|
// TODO: Add logic to scroll to the specific message in the channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Handle channel deep link
|
||||||
|
DeepLinkHandler.channelId != null -> {
|
||||||
|
val channelId = DeepLinkHandler.channelId!!
|
||||||
|
Log.d("ChatRouterScreen", "Setting destination to channel: $channelId")
|
||||||
|
val resolvedChannel = RevoltAPI.channelCache[channelId]
|
||||||
|
|
||||||
|
if (resolvedChannel == null) {
|
||||||
|
showChannelUnavailableAlert = true
|
||||||
|
} else {
|
||||||
|
viewModel.setSaveDestination(ChatRouterDestination.Channel(channelId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Handle server deep link
|
||||||
|
DeepLinkHandler.serverId != null -> {
|
||||||
|
val serverId = DeepLinkHandler.serverId!!
|
||||||
|
Log.d("ChatRouterScreen", "Setting destination to server: $serverId")
|
||||||
|
viewModel.navigateToServer(serverId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 4: Handle user deep link
|
||||||
|
DeepLinkHandler.userId != null -> {
|
||||||
|
val userId = DeepLinkHandler.userId!!
|
||||||
|
Log.d("ChatRouterScreen", "Setting destination to user: $userId")
|
||||||
|
// TODO: Implement user navigation if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the deep link data after processing
|
||||||
|
DeepLinkHandler.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
while (true) {
|
while (true) {
|
||||||
ActionChannel.receive().let { action ->
|
ActionChannel.receive().let { action ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
package chat.revolt.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import chat.revolt.activities.DeepLinkActivity
|
||||||
|
import chat.revolt.activities.MainActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for handling deep links in the Revolt app.
|
||||||
|
*/
|
||||||
|
object DeepLinkUtils {
|
||||||
|
private const val TAG = "DeepLinkUtils"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given URI is a valid Revolt deep link.
|
||||||
|
*
|
||||||
|
* @param uri The URI to check
|
||||||
|
* @return true if the URI is a valid Revolt deep link, false otherwise
|
||||||
|
*/
|
||||||
|
fun isRevoltDeepLink(uri: Uri?): Boolean {
|
||||||
|
if (uri == null) return false
|
||||||
|
|
||||||
|
val host = uri.host ?: return false
|
||||||
|
val scheme = uri.scheme ?: return false
|
||||||
|
|
||||||
|
// Check if the scheme is http or https
|
||||||
|
if (scheme != "http" && scheme != "https") return false
|
||||||
|
|
||||||
|
// Check if the host is a valid Revolt host
|
||||||
|
return host == "peptide.chat" || host == "app.peptide.chat"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the channel ID from a channel deep link URI.
|
||||||
|
*
|
||||||
|
* @param uri The URI to extract the channel ID from
|
||||||
|
* @return The channel ID, or null if the URI is not a valid channel deep link
|
||||||
|
*/
|
||||||
|
fun extractChannelId(uri: Uri?): String? {
|
||||||
|
if (uri == null) return null
|
||||||
|
|
||||||
|
val pathSegments = uri.pathSegments
|
||||||
|
|
||||||
|
// Handle /channels/CHANNEL_ID format
|
||||||
|
if (pathSegments.size >= 2 && pathSegments[0] == "channels") {
|
||||||
|
return pathSegments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle /server/SERVER_ID/channel/CHANNEL_ID format
|
||||||
|
if (pathSegments.size >= 4 &&
|
||||||
|
pathSegments[0] == "server" &&
|
||||||
|
pathSegments[2] == "channel") {
|
||||||
|
return pathSegments[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the server ID from a server deep link URI.
|
||||||
|
*
|
||||||
|
* @param uri The URI to extract the server ID from
|
||||||
|
* @return The server ID, or null if the URI is not a valid server deep link
|
||||||
|
*/
|
||||||
|
fun extractServerId(uri: Uri?): String? {
|
||||||
|
if (uri == null) return null
|
||||||
|
|
||||||
|
val pathSegments = uri.pathSegments
|
||||||
|
|
||||||
|
// Handle /servers/SERVER_ID format
|
||||||
|
if (pathSegments.size >= 2 && pathSegments[0] == "servers") {
|
||||||
|
return pathSegments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle /server/SERVER_ID format
|
||||||
|
if (pathSegments.size >= 2 && pathSegments[0] == "server") {
|
||||||
|
return pathSegments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the user ID from a user deep link URI.
|
||||||
|
*
|
||||||
|
* @param uri The URI to extract the user ID from
|
||||||
|
* @return The user ID, or null if the URI is not a valid user deep link
|
||||||
|
*/
|
||||||
|
fun extractUserId(uri: Uri?): String? {
|
||||||
|
if (uri == null) return null
|
||||||
|
|
||||||
|
val pathSegments = uri.pathSegments
|
||||||
|
if (pathSegments.size < 2 || pathSegments[0] != "users") return null
|
||||||
|
|
||||||
|
return pathSegments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the message ID from a message deep link URI.
|
||||||
|
*
|
||||||
|
* @param uri The URI to extract the message ID from
|
||||||
|
* @return The message ID, or null if the URI is not a valid message deep link
|
||||||
|
*/
|
||||||
|
fun extractMessageId(uri: Uri?): String? {
|
||||||
|
if (uri == null) return null
|
||||||
|
|
||||||
|
val pathSegments = uri.pathSegments
|
||||||
|
|
||||||
|
// Handle /messages/MESSAGE_ID format
|
||||||
|
if (pathSegments.size >= 2 && pathSegments[0] == "messages") {
|
||||||
|
return pathSegments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle /server/SERVER_ID/channel/CHANNEL_ID/MESSAGE_ID format
|
||||||
|
if (pathSegments.size >= 5 &&
|
||||||
|
pathSegments[0] == "server" &&
|
||||||
|
pathSegments[2] == "channel") {
|
||||||
|
return pathSegments[4]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class to hold server, channel, and message IDs extracted from a URI.
|
||||||
|
*/
|
||||||
|
data class ServerChannelMessage(
|
||||||
|
val serverId: String,
|
||||||
|
val channelId: String,
|
||||||
|
val messageId: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts server, channel, and message IDs from a server channel message deep link URI.
|
||||||
|
*
|
||||||
|
* @param uri The URI to extract the IDs from
|
||||||
|
* @return A ServerChannelMessage object containing the extracted IDs, or null if the URI is not a valid server channel message deep link
|
||||||
|
*/
|
||||||
|
fun extractServerChannelMessage(uri: Uri?): ServerChannelMessage? {
|
||||||
|
if (uri == null) return null
|
||||||
|
|
||||||
|
val pathSegments = uri.pathSegments
|
||||||
|
|
||||||
|
// Handle /server/SERVER_ID/channel/CHANNEL_ID format
|
||||||
|
if (pathSegments.size >= 4 &&
|
||||||
|
pathSegments[0] == "server" &&
|
||||||
|
pathSegments[2] == "channel") {
|
||||||
|
|
||||||
|
val serverId = pathSegments[1]
|
||||||
|
val channelId = pathSegments[3]
|
||||||
|
|
||||||
|
// Check if there's a message ID (5th segment)
|
||||||
|
val messageId = if (pathSegments.size >= 5) pathSegments[4] else null
|
||||||
|
|
||||||
|
return ServerChannelMessage(serverId, channelId, messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an intent to navigate to a specific channel.
|
||||||
|
*
|
||||||
|
* @param context The context to use for creating the intent
|
||||||
|
* @param channelId The ID of the channel to navigate to
|
||||||
|
* @return An intent that will navigate to the specified channel
|
||||||
|
*/
|
||||||
|
fun createChannelIntent(context: Context, channelId: String): Intent {
|
||||||
|
return Intent(context, MainActivity::class.java).apply {
|
||||||
|
putExtra(DeepLinkActivity.PATH_CHANNEL, channelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an intent to navigate to a specific server.
|
||||||
|
*
|
||||||
|
* @param context The context to use for creating the intent
|
||||||
|
* @param serverId The ID of the server to navigate to
|
||||||
|
* @return An intent that will navigate to the specified server
|
||||||
|
*/
|
||||||
|
fun createServerIntent(context: Context, serverId: String): Intent {
|
||||||
|
return Intent(context, MainActivity::class.java).apply {
|
||||||
|
putExtra(DeepLinkActivity.PATH_SERVER, serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an intent to navigate to a specific user.
|
||||||
|
*
|
||||||
|
* @param context The context to use for creating the intent
|
||||||
|
* @param userId The ID of the user to navigate to
|
||||||
|
* @return An intent that will navigate to the specified user
|
||||||
|
*/
|
||||||
|
fun createUserIntent(context: Context, userId: String): Intent {
|
||||||
|
return Intent(context, MainActivity::class.java).apply {
|
||||||
|
putExtra(DeepLinkActivity.PATH_USER, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an intent to navigate to a specific message.
|
||||||
|
*
|
||||||
|
* @param context The context to use for creating the intent
|
||||||
|
* @param messageId The ID of the message to navigate to
|
||||||
|
* @param channelId The ID of the channel containing the message, if known
|
||||||
|
* @return An intent that will navigate to the specified message
|
||||||
|
*/
|
||||||
|
fun createMessageIntent(context: Context, messageId: String, channelId: String? = null): Intent {
|
||||||
|
return Intent(context, MainActivity::class.java).apply {
|
||||||
|
putExtra(DeepLinkActivity.PATH_MESSAGE, messageId)
|
||||||
|
if (channelId != null) {
|
||||||
|
putExtra(DeepLinkActivity.PATH_CHANNEL, channelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue