add deep link

This commit is contained in:
2026-05-29 08:28:20 +07:00
parent 08058c0297
commit fea0b9154a
4 changed files with 123 additions and 103 deletions

View File

@@ -18,15 +18,30 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.App.Starting"> android:theme="@style/Theme.App.Starting">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<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:host="chat"
android:scheme="coop" />
</intent-filter>
</activity> </activity>
<service <service
android:name=".NostrForegroundService" android:name=".NostrForegroundService"
android:enabled="true" android:enabled="true"

View File

@@ -43,6 +43,7 @@ import androidx.navigation.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
import androidx.navigation.toRoute import androidx.navigation.toRoute
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import su.reya.coop.screens.ChatScreen import su.reya.coop.screens.ChatScreen
@@ -70,10 +71,7 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun App( fun App(viewModel: NostrViewModel) {
viewModel: NostrViewModel,
openRoomId: Long? = null
) {
val context = LocalContext.current val context = LocalContext.current
val navController = rememberNavController() val navController = rememberNavController()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -114,18 +112,94 @@ fun App(
LaunchedEffect(emptySecret) { LaunchedEffect(emptySecret) {
// Navigate to the home screen if the secret is already set // Navigate to the home screen if the secret is already set
if (emptySecret == false && openRoomId == null) { if (emptySecret == false) {
navController.navigate(Screen.Home) { navController.navigate(Screen.Home) {
popUpTo(Screen.Onboarding) { inclusive = true } popUpTo(Screen.Onboarding) { inclusive = true }
} }
} }
} }
LaunchedEffect(openRoomId) { NavHost(
if (openRoomId != null) { navController = navController,
navController.navigate(Screen.Chat(openRoomId)) { startDestination = if (emptySecret == false) Screen.Home else Screen.Onboarding
popUpTo(Screen.Home) { saveState = true } ) {
} composable<Screen.Onboarding> { backStackEntry ->
OnboardingScreen(
onOpenImport = { navController.navigate(Screen.Import) },
onOpenNew = { navController.navigate(Screen.NewIdentity) }
)
}
composable<Screen.Import> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
ImportScreen(
isLoading = isCreating,
onBack = { navController.popBackStack() },
onSave = { secret ->
viewModel.importIdentity(secret)
}
)
}
composable<Screen.NewIdentity> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
NewIdentityScreen(
isLoading = isCreating,
onBack = { navController.popBackStack() },
onSave = { name, bio, uri ->
val contentType = uri?.let { context.contentResolver.getType(it) }
val picture = uri?.let {
context.contentResolver.openInputStream(it)?.use { input ->
input.readBytes()
}
}
viewModel.createIdentity(name, bio, picture, contentType)
}
)
}
composable<Screen.Home> { backStackEntry ->
HomeScreen(
onOpenChat = { id -> navController.navigate(Screen.Chat(id)) },
onNewChat = { navController.navigate(Screen.NewChat) }
)
}
composable<Screen.Chat>(
deepLinks = listOf(
navDeepLink<Screen.Chat>(basePath = "coop://chat")
)
) { backStackEntry ->
val chat: Screen.Chat = backStackEntry.toRoute()
ChatScreen(
id = chat.id,
onBack = { navController.popBackStack() },
)
}
composable<Screen.Profile> { backStackEntry ->
val profile: Screen.Profile = backStackEntry.toRoute()
ProfileScreen(
pubkey = profile.pubkey,
onBack = { navController.popBackStack() },
)
}
composable<Screen.NewChat> { backStackEntry ->
NewChatScreen(
onBack = { navController.popBackStack() },
)
}
composable<Screen.Scan> { backStackEntry ->
ScanScreen(
onBack = { navController.popBackStack() },
)
}
composable<Screen.MyQr> { backStackEntry ->
MyQrScreen(
onBack = { navController.popBackStack() },
)
}
composable<Screen.Relay> { backStackEntry ->
RelayScreen(
onBack = { navController.popBackStack() },
)
} }
} }
@@ -183,86 +257,6 @@ fun App(
} }
} }
} }
NavHost(
navController = navController,
startDestination = if (emptySecret == false) Screen.Home else Screen.Onboarding
) {
composable<Screen.Onboarding> { backStackEntry ->
OnboardingScreen(
onOpenImport = { navController.navigate(Screen.Import) },
onOpenNew = { navController.navigate(Screen.NewIdentity) }
)
}
composable<Screen.Import> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
ImportScreen(
isLoading = isCreating,
onBack = { navController.popBackStack() },
onSave = { secret ->
viewModel.importIdentity(secret)
}
)
}
composable<Screen.NewIdentity> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
NewIdentityScreen(
isLoading = isCreating,
onBack = { navController.popBackStack() },
onSave = { name, bio, uri ->
val contentType = uri?.let { context.contentResolver.getType(it) }
val picture = uri?.let {
context.contentResolver.openInputStream(it)?.use { input ->
input.readBytes()
}
}
viewModel.createIdentity(name, bio, picture, contentType)
}
)
}
composable<Screen.Home> { backStackEntry ->
HomeScreen(
onOpenChat = { id -> navController.navigate(Screen.Chat(id)) },
onNewChat = { navController.navigate(Screen.NewChat) }
)
}
composable<Screen.Chat> { backStackEntry ->
val chat: Screen.Chat = backStackEntry.toRoute()
ChatScreen(
id = chat.id,
onBack = { navController.popBackStack() },
)
}
composable<Screen.Profile> { backStackEntry ->
val profile: Screen.Profile = backStackEntry.toRoute()
ProfileScreen(
pubkey = profile.pubkey,
onBack = { navController.popBackStack() },
)
}
composable<Screen.NewChat> { backStackEntry ->
NewChatScreen(
onBack = { navController.popBackStack() },
)
}
composable<Screen.Scan> { backStackEntry ->
ScanScreen(
onBack = { navController.popBackStack() },
)
}
composable<Screen.MyQr> { backStackEntry ->
MyQrScreen(
onBack = { navController.popBackStack() },
)
}
composable<Screen.Relay> { backStackEntry ->
RelayScreen(
onBack = { navController.popBackStack() },
)
}
}
} }
} }
} }

View File

@@ -6,10 +6,22 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import su.reya.coop.coop.storage.SecretStore import su.reya.coop.coop.storage.SecretStore
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val viewModel: NostrViewModel by viewModels {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val secretStore = SecretStore(this@MainActivity)
return NostrViewModel(NostrManager.instance, secretStore) as T
}
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
enableEdgeToEdge() enableEdgeToEdge()
@@ -17,25 +29,19 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val serviceIntent = Intent(this, NostrForegroundService::class.java) val serviceIntent = Intent(this, NostrForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent) startForegroundService(serviceIntent)
} else { } else {
startService(serviceIntent) startService(serviceIntent)
} }
val roomId = intent.getLongExtra("room_id", -1L)
val secretStore = SecretStore(this)
val viewModel = NostrViewModel(NostrManager.instance, secretStore)
splashScreen.setKeepOnScreenCondition { splashScreen.setKeepOnScreenCondition {
viewModel.emptySecret.value == null viewModel.emptySecret.value == null
} }
setContent { setContent {
App( App(viewModel = viewModel)
viewModel = viewModel,
openRoomId = if (roomId != -1L) roomId else null
)
} }
} }

View File

@@ -10,6 +10,7 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -111,10 +112,14 @@ class NostrForegroundService : Service() {
} }
private fun showNewMessageNotification(roomId: Long, message: String) { private fun showNewMessageNotification(roomId: Long, message: String) {
val intent = Intent(this, MainActivity::class.java).apply { val deepLinkUri = "coop://chat/$roomId".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("room_id", roomId) val intent = Intent(
} Intent.ACTION_VIEW,
deepLinkUri,
this,
MainActivity::class.java
)
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
this, this,