mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Mobile nostr notifications
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
38
android/app/src/main/java/com/robosats/Connectivity.kt
Normal file
38
android/app/src/main/java/com/robosats/Connectivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
80
android/app/src/main/java/com/robosats/models/NostrClient.kt
Normal file
80
android/app/src/main/java/com/robosats/models/NostrClient.kt
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
189
android/app/src/main/res/drawable/ic_notification.xml
Normal file
189
android/app/src/main/res/drawable/ic_notification.xml
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user