Mobile nostr notifications

This commit is contained in:
koalasat
2025-07-24 18:20:26 +02:00
parent 5437f75468
commit 639dff8481
11 changed files with 581 additions and 5 deletions

View File

@ -11,7 +11,7 @@ android {
defaultConfig {
applicationId = "com.robosats"
minSdk = 24
minSdk = 26
targetSdk = 36
versionCode = 15
versionName = "0.8.1-alpha"
@ -72,6 +72,8 @@ dependencies {
implementation(libs.material)
implementation(libs.okhttp)
implementation(libs.kmp.tor)
implementation(libs.quartz)
implementation(libs.ammolite)
// Add the KMP Tor binary dependency (contains the native .so files)
implementation(libs.kmp.tor.binary)
implementation(libs.androidx.activity)

View File

@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:extractNativeLibs="true"
@ -16,6 +20,16 @@
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Robosats">
<service
android:name=".services.NotificationsService"
android:enabled="true"
android:foregroundServiceType="specialUse"
android:exported="false">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Run a foreground service to check for notes and keep the connection to the relays active"
/>
</service>
<activity
android:name="com.robosats.MainActivity"
android:exported="true">

View File

@ -0,0 +1,38 @@
package com.robosats
import android.net.NetworkCapabilities
import com.vitorpamplona.ammolite.service.HttpClientManager
class Connectivity {
companion object {
var isOnMobileData: Boolean = false
var isOnWifiData: Boolean = false
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
val isOnMobileDataNet = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val isOnWifiNet = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
var changedNetwork = false
if (isOnMobileData != isOnMobileDataNet) {
isOnMobileData = isOnMobileDataNet
changedNetwork = true
}
if (isOnWifiData != isOnWifiNet) {
isOnWifiData = isOnWifiNet
changedNetwork = true
}
if (changedNetwork) {
if (isOnMobileDataNet) {
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
} else {
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
}
}
return changedNetwork
}
}
}

View File

@ -1,5 +1,6 @@
package com.robosats
import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.content.pm.ActivityInfo
@ -20,14 +21,19 @@ import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.robosats.services.NotificationsService
import com.robosats.tor.TorKmp
import com.robosats.tor.TorKmpManager
class MainActivity : AppCompatActivity() {
private val requestCodePostNotifications: Int = 1
private lateinit var webView: WebView
private lateinit var torKmp: TorKmp
private lateinit var loadingContainer: ConstraintLayout
@ -52,6 +58,28 @@ class MainActivity : AppCompatActivity() {
// Initialize Tor and setup WebView only after Tor is properly connected
initializeTor()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
requestCodePostNotifications,
)
}
}
private fun initializeNotifications() {
startForegroundService(
Intent(
this,
NotificationsService::class.java,
),
)
}
private fun initializeTor() {
@ -255,6 +283,8 @@ class MainActivity : AppCompatActivity() {
// Now it's safe to load the local HTML file
webView.loadUrl("file:///android_asset/index.html")
initializeNotifications()
}
} catch (e: Exception) {
Log.e("WebViewSetup", "Security error in WebView setup: ${e.message}", e)

View File

@ -0,0 +1,80 @@
package com.robosats.models
import android.util.Log
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayPool
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
object NostrClient {
private var subscriptionNotificationId = "robosatsNotificationId"
fun init() {
RelayPool.register(Client)
}
fun stop() {
RelayPool.unloadRelays()
}
fun start() {
connectRelays()
subscribeToInbox()
}
fun checkRelaysHealth() {
if (RelayPool.getAll().isEmpty()) {
stop()
start()
}
RelayPool.getAll().forEach {
if (!it.isConnected()) {
Log.d(
"RobosatsNostrClient",
"Relay ${it.url} is not connected, reconnecting...",
)
it.connectAndSendFiltersIfDisconnected()
}
}
}
private fun connectRelays() {
val relays = emptyList<String>()
relays.forEach {
Client.sendFilterOnlyIfDisconnected()
if (RelayPool.getRelays(it).isEmpty()) {
RelayPool.addRelay(
Relay(
it,
read = true,
write = false,
forceProxy = false,
activeTypes = COMMON_FEED_TYPES,
),
)
}
}
}
private fun subscribeToInbox() {
val authors = emptyList<String>()
if (authors.isNotEmpty()) {
Client.sendFilter(
subscriptionNotificationId,
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter = SincePerRelayFilter(
kinds = listOf(1, 4, 6, 7, 9735),
tags = mapOf("p" to authors),
),
),
),
)
}
}
}

View File

@ -0,0 +1,214 @@
package com.robosats.services
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationChannelGroupCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.robosats.Connectivity
import com.robosats.R
import com.robosats.models.NostrClient
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.events.Event
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.ConcurrentHashMap
class NotificationsService : Service() {
private var channelRelaysId = "RelaysConnections"
private var channelNotificationsId = "Notifications"
private lateinit var notificationGroup: NotificationChannelGroupCompat
private val timer = Timer()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val processedEvents = ConcurrentHashMap<String, Boolean>()
private val clientNotificationListener =
object : Client.Listener {
override fun onEvent(
event: Event,
subscriptionId: String,
relay: Relay,
afterEOSE: Boolean,
) {
if (processedEvents.putIfAbsent(event.id, true) == null) {
Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId - ${event.toJson()}")
val notify = true
if (notify) {
Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId - Broadcast")
}
}
}
}
private val networkCallback =
object : ConnectivityManager.NetworkCallback() {
var lastNetwork: Network? = null
override fun onAvailable(network: Network) {
super.onAvailable(network)
if (lastNetwork != null && lastNetwork != network) {
scope.launch(Dispatchers.IO) {
stopSubscription()
delay(1000)
startSubscription()
}
}
lastNetwork = network
}
// Network capabilities have changed for the network
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
super.onCapabilitiesChanged(network, networkCapabilities)
scope.launch(Dispatchers.IO) {
Log.d(
"RobosatsNotifications",
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${Connectivity.isOnMobileData} hasWifi ${Connectivity.isOnWifiData}",
)
if (Connectivity.updateNetworkCapabilities(networkCapabilities)) {
stopSubscription()
delay(1000)
startSubscription()
}
}
}
}
override fun onBind(intent: Intent): IBinder {
return null!!
}
override fun onCreate() {
val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.registerDefaultNetworkCallback(networkCallback)
NostrClient.init()
super.onCreate()
}
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startService()
return START_STICKY
}
override fun onDestroy() {
timer.cancel()
stopSubscription()
try {
val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (e: Exception) {
Log.d("RobosatsNotifications", "Failed to unregisterNetworkCallback", e)
}
super.onDestroy()
}
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
private fun startService() {
try {
Log.d("RobosatsNotifications", "Starting foreground service...")
startForeground(1, createNotification())
keepAlive()
startSubscription()
val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.registerDefaultNetworkCallback(networkCallback)
} catch (e: Exception) {
Log.e("NotificationsService", "Error in service", e)
}
}
private fun startSubscription() {
if (!Client.isSubscribed(clientNotificationListener)) Client.subscribe(clientNotificationListener)
CoroutineScope(Dispatchers.IO).launch {
NostrClient.start()
}
}
private fun stopSubscription() {
Client.unsubscribe(clientNotificationListener)
NostrClient.stop()
}
private fun keepAlive() {
timer.schedule(
object : TimerTask() {
override fun run() {
NostrClient.checkRelaysHealth()
}
},
5000,
61000,
)
}
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
private fun createNotification(): Notification {
val notificationManager = NotificationManagerCompat.from(this)
Log.d("RobosatsNotifications", "Building groups...")
notificationGroup = NotificationChannelGroupCompat.Builder("ServiceGroup")
.setName(getString(R.string.notifications))
.setDescription(getString(R.string.robosats_is_running_in_background))
.build()
notificationManager.createNotificationChannelGroup(notificationGroup)
Log.d("RobosatsNotifications", "Building channels...")
val channelRelays = NotificationChannelCompat.Builder(channelRelaysId, NotificationManager.IMPORTANCE_DEFAULT)
.setName(getString(R.string.service))
.setGroup(notificationGroup.id)
.build()
val channelNotification = NotificationChannelCompat.Builder(channelNotificationsId, NotificationManager.IMPORTANCE_HIGH)
.setName(getString(R.string.notifications))
.setGroup(notificationGroup.id)
.build()
notificationManager.createNotificationChannel(channelRelays)
notificationManager.createNotificationChannel(channelNotification)
Log.d("RobosatsNotifications", "Building notification...")
val notificationBuilder =
NotificationCompat.Builder(this, channelRelaysId)
.setContentTitle(getString(R.string.robosats_is_running_in_background))
.setPriority(NotificationCompat.PRIORITY_MIN)
.setGroup(notificationGroup.id)
.setSmallIcon(R.drawable.ic_notification)
val build = notificationBuilder.build()
notificationManager.notify(1, build)
return build
}
}

View File

@ -0,0 +1,189 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:pivotX="54"
android:pivotY="54"
android:scaleX="2"
android:scaleY="2">
<path
android:pathData="m33.8,64.92c-0,1.93 -0,3.8 -0,5.54 0.56,-0.52 1.19,-1.08 1.79,-1.66 0.19,-0.18 0.32,-0.2 0.51,-0.03 0.32,0.3 0.66,0.59 1,0.89 0.31,-0.35 0.6,-0.68 0.9,-1.02 -1.4,-1.25 -2.79,-2.47 -4.2,-3.72z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="8.42"
android:startY="37.07"
android:endX="68.63"
android:endY="108.24"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m46.15,75.88c-1.39,-1.23 -2.73,-2.43 -4.09,-3.64 -0.31,0.35 -0.59,0.67 -0.89,1.01 0.38,0.34 0.75,0.67 1.14,1.02 -0.52,0.53 -1.02,1.05 -1.56,1.61 1.82,0 3.58,0 5.4,0z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="4.58"
android:startY="29.94"
android:endX="68.04"
android:endY="104.95"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m59.78,57.16c1.14,-0.58 2.21,-1.26 3.19,-2.08 1.97,-1.66 3.5,-3.63 4.23,-6.14 0.62,-2.14 0.76,-4.33 0.54,-6.54 -0.15,-1.53 -0.56,-2.99 -1.4,-4.31 -1.79,-2.82 -4.4,-4.51 -7.6,-5.31 -2.23,-0.55 -4.51,-0.66 -6.79,-0.67 -5.94,-0.02 -11.88,-0.01 -17.82,-0.02 -0.11,-0 -0.21,0.01 -0.32,0.01 -0,6.14 -0.01,12.24 -0.01,18.34 3.93,-2.96 7.97,-3.55 12.11,-0.64 0.54,-0.51 1.08,-1 1.64,-1.52 -0.57,-0.41 -1.11,-0.81 -1.67,-1.22 1.42,-1.03 2.8,-2.03 4.18,-3.03 -0.37,-0.85 -0.22,-1.52 0.4,-1.92 0.54,-0.35 1.29,-0.25 1.72,0.22 0.44,0.48 0.47,1.22 0.08,1.73 -0.44,0.59 -1.14,0.68 -1.94,0.25 -0.7,0.86 -1.39,1.71 -2.11,2.59 0.59,0.42 1.15,0.82 1.74,1.23 -1.15,0.77 -2.26,1.52 -3.39,2.28 0.04,0.05 0.06,0.08 0.08,0.1 4.1,3.64 8.2,7.28 12.3,10.91 0.88,0.78 1.77,1.54 2.39,2.55 1.66,2.72 1.77,5.55 0.63,8.47 -0.5,1.28 -1.26,2.41 -2.28,3.44 4.87,0 9.67,0.01 14.53,0.01 -4.91,-6.22 -9.79,-12.4 -14.68,-18.59 0.1,-0.06 0.17,-0.1 0.25,-0.14z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="34.66"
android:startY="25.97"
android:endX="82.65"
android:endY="82.7"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m38.45,60.04c0.23,0.2 0.48,0.35 0.74,0.46 -0.43,-0.37 -0.85,-0.72 -1.27,-1.07 0.14,0.22 0.32,0.43 0.53,0.61z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="22.08"
android:startY="40.5"
android:endX="64.93"
android:endY="91.16"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m38.45,60.04c0.23,0.2 0.48,0.35 0.74,0.46 -0.43,-0.37 -0.85,-0.72 -1.27,-1.07 0.14,0.22 0.32,0.43 0.53,0.61z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="22.09"
android:startY="40.52"
android:endX="64.94"
android:endY="91.17"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m42.5,59.76c1.04,-1.18 0.91,-2.98 -0.28,-4.01 -1.2,-1.03 -3.01,-0.91 -4.05,0.28 -0.86,0.98 -0.92,2.36 -0.25,3.4 0.42,0.35 0.84,0.7 1.27,1.07 1.12,0.48 2.47,0.22 3.31,-0.74z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="22.23"
android:startY="36.48"
android:endX="70.22"
android:endY="93.21"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m42.5,59.76c1.04,-1.18 0.91,-2.98 -0.28,-4.01 -1.2,-1.03 -3.01,-0.91 -4.05,0.28 -0.86,0.98 -0.92,2.36 -0.25,3.4 0.42,0.35 0.84,0.7 1.27,1.07 1.12,0.48 2.47,0.22 3.31,-0.74z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="24.15"
android:startY="38.75"
android:endX="67"
android:endY="89.4"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m42.5,59.76c1.04,-1.18 0.91,-2.98 -0.28,-4.01 -1.2,-1.03 -3.01,-0.91 -4.05,0.28 -0.86,0.98 -0.92,2.36 -0.25,3.4 0.42,0.35 0.84,0.7 1.27,1.07 1.12,0.48 2.47,0.22 3.31,-0.74z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="24.16"
android:startY="38.76"
android:endX="67.01"
android:endY="89.41"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m54.88,66.66c-1.19,-1.03 -3.01,-0.91 -4.05,0.28 -1.04,1.18 -0.91,2.98 0.28,4.01 1.2,1.03 3.01,0.91 4.05,-0.28 1.04,-1.18 0.91,-2.98 -0.28,-4.01z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="24.23"
android:startY="34.79"
android:endX="72.22"
android:endY="91.52"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m54.88,66.66c-1.19,-1.03 -3.01,-0.91 -4.05,0.28 -1.04,1.18 -0.91,2.98 0.28,4.01 1.2,1.03 3.01,0.91 4.05,-0.28 1.04,-1.18 0.91,-2.98 -0.28,-4.01z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="26.15"
android:startY="37.07"
android:endX="69"
android:endY="87.73"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m54.88,66.66c-1.19,-1.03 -3.01,-0.91 -4.05,0.28 -1.04,1.18 -0.91,2.98 0.28,4.01 1.2,1.03 3.01,0.91 4.05,-0.28 1.04,-1.18 0.91,-2.98 -0.28,-4.01z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="26.16"
android:startY="37.08"
android:endX="69.01"
android:endY="87.74"
android:type="linear">
<item android:offset="0.33" android:color="#FF1976D2"/>
<item android:offset="0.42" android:color="#FF2E69CC"/>
<item android:offset="0.61" android:color="#FF6548BE"/>
<item android:offset="0.78" android:color="#FF9C27B0"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@ -1,3 +1,8 @@
<resources>
<string name="app_name">Robosats</string>
<string name="service">Background Service</string>
<string name="connection">Connection</string>
<string name="configuration">Configuration</string>
<string name="robosats_is_running_in_background">Robosats is running in background fetching for notifications</string>
<string name="notifications">Notifications</string>
</resources>

View File

@ -12,6 +12,7 @@ constraintlayout = "2.2.1"
kmpTor= "4.8.10-0-1.4.5"
kmpTorBinary= "4.8.10-0"
okhttp = "5.0.0-alpha.14"
quartz = "0.92.7"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -25,6 +26,8 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
kmp-tor = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor", version.ref = "kmpTor" }
kmp-tor-binary = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor-binary-extract-android", version.ref = "kmpTorBinary" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
quartz = { module = "com.github.vitorpamplona.amethyst:quartz", version.ref = "quartz" }
ammolite = { module = "com.github.vitorpamplona.amethyst:ammolite", version.ref = "quartz" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

@ -3,16 +3,17 @@ pluginManagement {
google ()
mavenCentral()
gradlePluginPortal()
jcenter()
maven("https://mvnrepository.com")
maven("https://jitpack.io")
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter()
maven("https://mvnrepository.com")
maven("https://jitpack.io")
}
}

View File

@ -19,7 +19,7 @@ const RobotPage = (): React.JSX.Element => {
const [inputToken, setInputToken] = useState<string>('');
const [view, setView] = useState<'welcome' | 'onboarding' | 'profile'>(
garage.currentSlot !== null ? 'profile' : 'welcome',
Object.keys(garage.slots).length > 0 ? 'profile' : 'welcome',
);
useEffect(() => {
@ -30,7 +30,7 @@ const RobotPage = (): React.JSX.Element => {
}, [torStatus, page, slotUpdatedAt]);
useEffect(() => {
if (garage.currentSlot && view === 'welcome') setView('profile');
if (Object.keys(garage.slots).length > 0 && view === 'welcome') setView('profile');
}, [garage.currentSlot]);
return (