feat: implement basic notification #6
@@ -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"
|
||||||
|
|||||||
@@ -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() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user