mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 19:33:01 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -21,7 +21,7 @@ jobs:
|
||||
java-version: 17
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: ./gradlew assembleDebug --no-daemon
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Debug APK
|
||||
path: app/build/outputs/apk/play/debug/app-play-universal-debug.apk
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v1.20.4
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
|
||||
+24
@@ -1,6 +1,30 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.0.10" />
|
||||
<option name="version" value="2.0.20" />
|
||||
</component>
|
||||
</project>
|
||||
+2
-2
@@ -17,8 +17,8 @@ android {
|
||||
applicationId "com.greenart7c3.nostrsigner"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 84
|
||||
versionName "1.3.0"
|
||||
versionCode 91
|
||||
versionName "1.3.7"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -18,8 +18,13 @@
|
||||
<service
|
||||
android:name=".service.ConnectivityService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"></service>
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Run a foreground service to keep the connection to the relays active"
|
||||
/>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:exported="true"
|
||||
|
||||
@@ -20,12 +20,9 @@
|
||||
*/
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.database.LogEntity
|
||||
@@ -54,8 +51,8 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
message: ByteArray,
|
||||
instance: String,
|
||||
) {
|
||||
val messageStr = String(message)
|
||||
Log.d(TAG, "New message ${message.decodeToString()} for Instance: $instance")
|
||||
val messageStr = message.decodeToString()
|
||||
Log.d(TAG, "New message $messageStr for Instance: $instance")
|
||||
scope.launch {
|
||||
try {
|
||||
parseMessage(messageStr)?.let { receiveIfNew(it) }
|
||||
@@ -86,7 +83,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
instance: String,
|
||||
) {
|
||||
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance")
|
||||
val sanitizedEndpoint = endpoint.dropLast(5)
|
||||
val sanitizedEndpoint = if (endpoint.endsWith("?up=1")) endpoint.dropLast(5) else endpoint
|
||||
if (pushHandler.getSavedEndpoint() == sanitizedEndpoint) {
|
||||
Log.d(TAG, "Endpoint already saved. ")
|
||||
return
|
||||
@@ -98,16 +95,6 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
val intentData = intent.dataString
|
||||
val intentAction = intent.action.toString()
|
||||
Log.d(TAG, "Intent Data:- $intentData Intent Action: $intentAction")
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
|
||||
override fun onRegistrationFailed(
|
||||
context: Context,
|
||||
instance: String,
|
||||
@@ -140,9 +127,4 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
pushHandler.forceRemoveDistributor(context)
|
||||
pushHandler.removeEndpoint()
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager {
|
||||
return ContextCompat.getSystemService(appContext, NotificationManager::class.java)
|
||||
as NotificationManager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ object PushNotificationUtils {
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun init(accounts: List<AccountInfo>) {
|
||||
if (hasInit) {
|
||||
if (hasInit || NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,12 @@ object LocalPreferences {
|
||||
}.apply()
|
||||
}
|
||||
|
||||
suspend fun isNotificationTypeConfigured(): Boolean {
|
||||
val context = NostrSigner.getInstance()
|
||||
val prefs = encryptedPreferences(context)
|
||||
return prefs.contains(PrefKeys.NOTIFICATION_TYPE)
|
||||
}
|
||||
|
||||
suspend fun loadSettingsFromEncryptedStorage(): AmberSettings {
|
||||
val context = NostrSigner.getInstance()
|
||||
|
||||
@@ -130,7 +136,7 @@ object LocalPreferences {
|
||||
} ?: listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)),
|
||||
lastBiometricsTime = getLong(PrefKeys.LAST_BIOMETRICS_TIME, 0),
|
||||
useAuth = getBoolean(PrefKeys.USE_AUTH, false),
|
||||
notificationType = parseNotificationType(getInt(PrefKeys.NOTIFICATION_TYPE, 0)),
|
||||
notificationType = parseNotificationType(getInt(PrefKeys.NOTIFICATION_TYPE, 1)),
|
||||
biometricsTimeType = parseBiometricsTimeType(getInt(PrefKeys.BIOMETRICS_TYPE, 0)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -49,10 +50,11 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var mainViewModel: MainViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
installSplashScreen()
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
mainViewModel = MainViewModel(applicationContext)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
installSplashScreen()
|
||||
|
||||
HttpClientManager.setDefaultUserAgent("Amber/${BuildConfig.VERSION_NAME}")
|
||||
|
||||
@@ -165,17 +167,14 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
mainViewModel.showBunkerRequests(callingPackage)
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
val connectivityManager =
|
||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
||||
connectivityManager.registerDefaultNetworkCallback(mainViewModel.networkCallback)
|
||||
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
|
||||
mainViewModel.updateNetworkCapabilities(it)
|
||||
NostrSigner.getInstance().updateNetworkCapabilities(it)
|
||||
}
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,78 +3,22 @@ package com.greenart7c3.nostrsigner
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.NotificationDataSource
|
||||
import com.greenart7c3.nostrsigner.ui.NotificationType
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class MainViewModel(val context: Context) : ViewModel() {
|
||||
val intents = MutableStateFlow<List<IntentData>>(listOf())
|
||||
val isOnMobileDataState = mutableStateOf(false)
|
||||
val isOnWifiDataState = mutableStateOf(false)
|
||||
|
||||
val networkCallback =
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
var lastNetwork: Network? = null
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "offline") return
|
||||
|
||||
if (lastNetwork != null && lastNetwork != network && NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
NotificationDataSource.stopSync()
|
||||
delay(1000)
|
||||
NotificationDataSource.start()
|
||||
}
|
||||
}
|
||||
|
||||
lastNetwork = network
|
||||
}
|
||||
|
||||
// Network capabilities have changed for the network
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "offline") return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Log.d(
|
||||
"ServiceManager NetworkCallback",
|
||||
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${isOnMobileDataState.value} hasWifi ${isOnWifiDataState.value}",
|
||||
)
|
||||
if (updateNetworkCapabilities(networkCapabilities) && NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
NotificationDataSource.stopSync()
|
||||
delay(1000)
|
||||
NotificationDataSource.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAccount(): String? {
|
||||
val pubKeys =
|
||||
@@ -140,33 +84,4 @@ class MainViewModel(val context: Context) : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
|
||||
val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
var changedNetwork = false
|
||||
|
||||
if (isOnMobileDataState.value != isOnMobileData) {
|
||||
isOnMobileDataState.value = isOnMobileData
|
||||
|
||||
changedNetwork = true
|
||||
}
|
||||
|
||||
if (isOnWifiDataState.value != isOnWifi) {
|
||||
isOnWifiDataState.value = isOnWifi
|
||||
|
||||
changedNetwork = true
|
||||
}
|
||||
|
||||
if (changedNetwork) {
|
||||
if (isOnMobileData) {
|
||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
|
||||
} else {
|
||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
|
||||
}
|
||||
}
|
||||
|
||||
return changedNetwork
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.greenart7c3.nostrsigner
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.NetworkCapabilities
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
@@ -17,13 +19,50 @@ import com.vitorpamplona.ammolite.relays.RelayPool
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class NostrSigner : Application() {
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var databases = ConcurrentHashMap<String, AppDatabase>()
|
||||
lateinit var settings: AmberSettings
|
||||
|
||||
val isOnMobileDataState = mutableStateOf(false)
|
||||
val isOnWifiDataState = mutableStateOf(false)
|
||||
|
||||
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
|
||||
val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
var changedNetwork = false
|
||||
|
||||
if (isOnMobileDataState.value != isOnMobileData) {
|
||||
isOnMobileDataState.value = isOnMobileData
|
||||
|
||||
changedNetwork = true
|
||||
}
|
||||
|
||||
if (isOnWifiDataState.value != isOnWifi) {
|
||||
isOnWifiDataState.value = isOnWifi
|
||||
|
||||
changedNetwork = true
|
||||
}
|
||||
|
||||
if (changedNetwork) {
|
||||
if (isOnMobileData) {
|
||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
|
||||
} else {
|
||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
|
||||
}
|
||||
}
|
||||
|
||||
return changedNetwork
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
HttpClientManager.setDefaultUserAgent("Amber/${BuildConfig.VERSION_NAME}")
|
||||
@@ -66,6 +105,9 @@ class NostrSigner : Application() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (savedRelays.isEmpty()) {
|
||||
savedRelays.addAll(settings.defaultRelays)
|
||||
}
|
||||
return savedRelays
|
||||
}
|
||||
|
||||
@@ -126,6 +168,11 @@ class NostrSigner : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
applicationIOScope.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var instance: NostrSigner? = null
|
||||
|
||||
@@ -127,14 +127,18 @@ class SignerProvider : ContentProvider() {
|
||||
)
|
||||
if (permission == null) {
|
||||
event.kind.kindToNip()?.let {
|
||||
permission =
|
||||
val nipNumber = it.toIntOrNull()
|
||||
permission = if (nipNumber == null) {
|
||||
null
|
||||
} else {
|
||||
database
|
||||
.applicationDao()
|
||||
.getPermission(
|
||||
sortOrder ?: packageName,
|
||||
"NIP",
|
||||
it.toInt(),
|
||||
nipNumber,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val signPolicy = database.applicationDao().getSignPolicy(sortOrder ?: packageName)
|
||||
|
||||
@@ -11,6 +11,6 @@ data class AmberSettings(
|
||||
val defaultRelays: List<RelaySetupInfo> = listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)),
|
||||
val lastBiometricsTime: Long = 0,
|
||||
val useAuth: Boolean = false,
|
||||
val notificationType: NotificationType = NotificationType.PUSH,
|
||||
val notificationType: NotificationType = NotificationType.DIRECT,
|
||||
val biometricsTimeType: BiometricsTimeType = BiometricsTimeType.EVERY_TIME,
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ data class Permission(
|
||||
val kind: Int?,
|
||||
var checked: Boolean = true,
|
||||
) {
|
||||
// LAST_COMMIT_CHECKED = "14ec14dac92d891715239bfb83ed1c9bdf2ac382"
|
||||
fun toLocalizedString(context: Context): String {
|
||||
return when (type) {
|
||||
"get_public_key" -> context.getString(R.string.read_your_public_key)
|
||||
@@ -56,6 +57,8 @@ data class Permission(
|
||||
1971 -> context.getString(R.string.event_kind_1971)
|
||||
1984 -> context.getString(R.string.event_kind_1984)
|
||||
1985 -> context.getString(R.string.event_kind_1985)
|
||||
1986 -> context.getString(R.string.event_kind_1986)
|
||||
1987 -> context.getString(R.string.event_kind_1987)
|
||||
4550 -> context.getString(R.string.event_kind_4550)
|
||||
in 5000..5999 -> context.getString(R.string.event_kind_5000_5999)
|
||||
in 6000..6999 -> context.getString(R.string.event_kind_6000_6999)
|
||||
@@ -71,6 +74,7 @@ data class Permission(
|
||||
9021 -> context.getString(R.string.event_kind_9021)
|
||||
in 9008..9030 -> context.getString(R.string.event_kind_9000_9030)
|
||||
9041 -> context.getString(R.string.event_kind_9041)
|
||||
9467 -> context.getString(R.string.event_kind_9467)
|
||||
9734 -> context.getString(R.string.event_kind_9734)
|
||||
9735 -> context.getString(R.string.event_kind_9735)
|
||||
9802 -> context.getString(R.string.event_kind_9802)
|
||||
@@ -87,6 +91,7 @@ data class Permission(
|
||||
10015 -> context.getString(R.string.event_kind_10015)
|
||||
10030 -> context.getString(R.string.event_kind_10030)
|
||||
10050 -> context.getString(R.string.event_kind_10050)
|
||||
10063 -> context.getString(R.string.event_kind_10063)
|
||||
10096 -> context.getString(R.string.event_kind_10096)
|
||||
13194 -> context.getString(R.string.event_kind_13194)
|
||||
21000 -> context.getString(R.string.event_kind_21000)
|
||||
@@ -94,6 +99,7 @@ data class Permission(
|
||||
23194 -> context.getString(R.string.event_kind_23194)
|
||||
23195 -> context.getString(R.string.event_kind_23195)
|
||||
24133 -> context.getString(R.string.event_kind_24133)
|
||||
24242 -> context.getString(R.string.event_kind_24242)
|
||||
27235 -> context.getString(R.string.event_kind_27235)
|
||||
30000 -> context.getString(R.string.event_kind_30000)
|
||||
30001 -> context.getString(R.string.event_kind_30001)
|
||||
@@ -110,6 +116,8 @@ data class Permission(
|
||||
30023 -> context.getString(R.string.event_kind_30023)
|
||||
30024 -> context.getString(R.string.event_kind_30024)
|
||||
30030 -> context.getString(R.string.event_kind_30030)
|
||||
30040 -> context.getString(R.string.event_kind_30040)
|
||||
30041 -> context.getString(R.string.event_kind_30041)
|
||||
30063 -> context.getString(R.string.event_kind_30063)
|
||||
30078 -> context.getString(R.string.event_kind_30078)
|
||||
30311 -> context.getString(R.string.event_kind_30311)
|
||||
@@ -295,11 +303,16 @@ val kindsByNip = listOf(
|
||||
KindNip(30402..30403, "99"),
|
||||
KindNip(30617..30617, "34"),
|
||||
KindNip(30818..30819, "54"),
|
||||
KindNip(31890..31890, "https://wikifreedia.xyz/cip-01/97c70a44366a6535c1"),
|
||||
KindNip(31890..31890, "https://wikifreedia.xyz/cip-01"),
|
||||
KindNip(31922..31925, "52"),
|
||||
KindNip(31989..31990, "89"),
|
||||
KindNip(34235..34237, "71"),
|
||||
KindNip(34550..34550, "72"),
|
||||
KindNip(39000..39009, "29"),
|
||||
KindNip(31234..31234, "https://github.com/nostr-protocol/nips/pull/1124"),
|
||||
KindNip(9467..9467, "https://wikistr.com/tidal-nostr"),
|
||||
KindNip(24242..24242, "https://wikistr.com/blossom"),
|
||||
KindNip(10063..10063, "https://wikistr.com/blossom"),
|
||||
KindNip(30040..30041, "https://wikistr.com/nkbip-01"),
|
||||
KindNip(1987..1987, "https://wikistr.com/nkbip-02"),
|
||||
)
|
||||
|
||||
@@ -6,9 +6,13 @@ import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
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.core.app.NotificationCompat
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
@@ -19,6 +23,7 @@ import java.util.TimerTask
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ConnectivityService : Service() {
|
||||
@@ -26,6 +31,50 @@ class ConnectivityService : Service() {
|
||||
private val timer = Timer()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
private val networkCallback =
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
var lastNetwork: Network? = null
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "offline") return
|
||||
|
||||
if (lastNetwork != null && lastNetwork != network && NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NotificationDataSource.stopSync()
|
||||
delay(1000)
|
||||
NotificationDataSource.start()
|
||||
}
|
||||
}
|
||||
|
||||
lastNetwork = network
|
||||
}
|
||||
|
||||
// Network capabilities have changed for the network
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "offline") return
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d(
|
||||
"ServiceManager NetworkCallback",
|
||||
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${NostrSigner.getInstance().isOnMobileDataState.value} hasWifi ${NostrSigner.getInstance().isOnWifiDataState.value}",
|
||||
)
|
||||
if (NostrSigner.getInstance().updateNetworkCapabilities(networkCapabilities) && NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
NotificationDataSource.stopSync()
|
||||
delay(1000)
|
||||
NotificationDataSource.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return null!!
|
||||
}
|
||||
@@ -58,6 +107,16 @@ class ConnectivityService : Service() {
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(this@ConnectivityService))
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
val connectivityManager =
|
||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
||||
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
||||
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
|
||||
NostrSigner.getInstance().updateNetworkCapabilities(it)
|
||||
}
|
||||
}
|
||||
|
||||
timer.schedule(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
@@ -84,6 +143,16 @@ class ConnectivityService : Service() {
|
||||
override fun onDestroy() {
|
||||
isStarted = false
|
||||
timer.cancel()
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
try {
|
||||
val connectivityManager =
|
||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
} catch (e: Exception) {
|
||||
Log.d("connectivityManager", "Failed to unregisterNetworkCallback", e)
|
||||
}
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun consume(event: GiftWrapEvent) {
|
||||
fun consume(event: GiftWrapEvent) {
|
||||
if (!notificationManager().areNotificationsEnabled()) return
|
||||
|
||||
// PushNotification Wraps don't include a receiver.
|
||||
@@ -85,7 +85,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
pushWrappedEvent: GiftWrapEvent,
|
||||
account: Account,
|
||||
) {
|
||||
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
|
||||
pushWrappedEvent.unwrap(account.signer) { notificationEvent ->
|
||||
unwrapAndConsume(notificationEvent, account) { innerEvent ->
|
||||
if (innerEvent.kind == 24133) {
|
||||
notify(innerEvent, account)
|
||||
@@ -101,10 +101,10 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
) {
|
||||
when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
event.cachedGift(account.signer) { unwrapAndConsume(it, account, onReady) }
|
||||
event.unwrap(account.signer) { unwrapAndConsume(it, account, onReady) }
|
||||
}
|
||||
is SealedGossipEvent -> {
|
||||
event.cachedGossip(account.signer) {
|
||||
event.unseal(account.signer) {
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,6 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
permission.application.relays,
|
||||
onLoading = {},
|
||||
onDone = {},
|
||||
checkForEmptyRelays = false,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -221,10 +220,9 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
acc,
|
||||
bunkerRequest,
|
||||
BunkerResponse(bunkerRequest.id, "", message),
|
||||
applicationWithSecret?.application?.relays ?: listOf(),
|
||||
applicationWithSecret?.application?.relays ?: NostrSigner.getInstance().getSavedRelays().toList(),
|
||||
onLoading = { },
|
||||
onDone = { },
|
||||
checkForEmptyRelays = false,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -263,7 +261,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
if (type == SignerType.CONNECT) {
|
||||
message = "$name ${bunkerPermission.toLocalizedString(applicationContext)}"
|
||||
}
|
||||
val relays = permission?.application?.relays ?: applicationWithSecret?.application?.relays ?: listOf()
|
||||
val relays = permission?.application?.relays ?: applicationWithSecret?.application?.relays ?: NostrSigner.getInstance().getSavedRelays().toList()
|
||||
|
||||
if (permission == null && applicationWithSecret == null && !acc.allowNewConnections) {
|
||||
IntentUtils.sendBunkerResponse(
|
||||
@@ -271,10 +269,9 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
acc,
|
||||
bunkerRequest,
|
||||
BunkerResponse(bunkerRequest.id, "", "no permission"),
|
||||
listOf(),
|
||||
relays,
|
||||
onLoading = { },
|
||||
onDone = {},
|
||||
checkForEmptyRelays = false,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -302,7 +299,6 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
relays,
|
||||
onLoading = { },
|
||||
onDone = { },
|
||||
checkForEmptyRelays = false,
|
||||
)
|
||||
} else {
|
||||
val index = localCursor.getColumnIndex("event")
|
||||
@@ -316,7 +312,6 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
relays,
|
||||
onLoading = { },
|
||||
onDone = { },
|
||||
checkForEmptyRelays = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.database.LogEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.BunkerRequest
|
||||
import com.greenart7c3.nostrsigner.models.BunkerResponse
|
||||
@@ -55,6 +56,7 @@ data class BunkerMetadata(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val description: String,
|
||||
val perms: String,
|
||||
) {
|
||||
companion object {
|
||||
val mapper: ObjectMapper =
|
||||
@@ -70,6 +72,7 @@ data class BunkerMetadata(
|
||||
name = jsonObject.get("name")?.asText()?.intern() ?: "",
|
||||
url = jsonObject.get("url")?.asText()?.intern() ?: "",
|
||||
description = jsonObject.get("description")?.asText()?.intern() ?: "",
|
||||
perms = jsonObject.get("perms")?.asText()?.intern() ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -313,14 +316,7 @@ object IntentUtils {
|
||||
relays: List<RelaySetupInfo>,
|
||||
onLoading: (Boolean) -> Unit,
|
||||
onDone: () -> Unit,
|
||||
checkForEmptyRelays: Boolean = true,
|
||||
) {
|
||||
if (relays.isEmpty() && checkForEmptyRelays) {
|
||||
onLoading(false)
|
||||
AmberListenerSingleton.accountStateViewModel?.toast("Relays", "No relays found")
|
||||
return
|
||||
}
|
||||
|
||||
AmberListenerSingleton.getListener()?.let {
|
||||
Client.unsubscribe(it)
|
||||
}
|
||||
@@ -342,9 +338,7 @@ object IntentUtils {
|
||||
account,
|
||||
bunkerRequest.localKey,
|
||||
encryptedContent,
|
||||
relays,
|
||||
checkForEmptyRelays,
|
||||
context,
|
||||
relays.ifEmpty { NostrSigner.getInstance().getSavedRelays().toList() },
|
||||
onLoading,
|
||||
onDone,
|
||||
)
|
||||
@@ -359,9 +353,7 @@ object IntentUtils {
|
||||
account,
|
||||
bunkerRequest.localKey,
|
||||
encryptedContent,
|
||||
relays,
|
||||
checkForEmptyRelays,
|
||||
context,
|
||||
relays.ifEmpty { NostrSigner.getInstance().getSavedRelays().toList() },
|
||||
onLoading,
|
||||
onDone,
|
||||
)
|
||||
@@ -376,8 +368,6 @@ object IntentUtils {
|
||||
localKey: String,
|
||||
encryptedContent: String,
|
||||
relays: List<RelaySetupInfo>,
|
||||
checkForEmptyRelays: Boolean,
|
||||
context: Context,
|
||||
onLoading: (Boolean) -> Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
@@ -388,7 +378,7 @@ object IntentUtils {
|
||||
encryptedContent,
|
||||
) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (!checkForEmptyRelays || RelayPool.getAll().any { !it.isConnected() }) {
|
||||
if (RelayPool.getAll().any { !it.isConnected() }) {
|
||||
NostrSigner.getInstance().checkForNewRelays(
|
||||
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
|
||||
newRelays = relays.toSet(),
|
||||
@@ -803,22 +793,74 @@ object IntentUtils {
|
||||
val pubKey = split.first()
|
||||
val parsedData = URLDecoder.decode(split.drop(1).joinToString { it }.replace("+", "%2b"), "utf-8")
|
||||
val splitParsedData = parsedData.split("&")
|
||||
val permissions = mutableListOf<Permission>()
|
||||
splitParsedData.forEach {
|
||||
val internalSplit = it.split("=")
|
||||
if (internalSplit.first() == "relay") {
|
||||
val paramName = internalSplit.first()
|
||||
val json = internalSplit.mapIndexedNotNull { index, s ->
|
||||
if (index == 0) null else s
|
||||
}.joinToString { data -> data }
|
||||
if (paramName == "relay") {
|
||||
relays.add(
|
||||
RelaySetupInfo(internalSplit[1], read = true, write = true, feedTypes = COMMON_FEED_TYPES),
|
||||
RelaySetupInfo(json, read = true, write = true, feedTypes = COMMON_FEED_TYPES),
|
||||
)
|
||||
}
|
||||
if (internalSplit.first() == "name") {
|
||||
name = internalSplit[1]
|
||||
if (paramName == "name") {
|
||||
name = json
|
||||
}
|
||||
if (internalSplit.first() == "metadata") {
|
||||
val bunkerMetada = metaDataFromJson(internalSplit[1])
|
||||
|
||||
if (paramName == "perms") {
|
||||
if (json.isNotEmpty()) {
|
||||
val splitPerms = json.split(",")
|
||||
splitPerms.forEach {
|
||||
val split2 = it.split(":")
|
||||
val permissionType = split2.first()
|
||||
val kind =
|
||||
try {
|
||||
split2[1].toInt()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
permissions.add(
|
||||
Permission(
|
||||
permissionType,
|
||||
kind,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (paramName == "metadata") {
|
||||
val bunkerMetada = metaDataFromJson(json)
|
||||
name = bunkerMetada.name
|
||||
if (bunkerMetada.perms.isNotEmpty()) {
|
||||
val splitPerms = bunkerMetada.perms.split(",")
|
||||
splitPerms.forEach {
|
||||
val split2 = it.split(":")
|
||||
val permissionType = split2.first()
|
||||
val kind =
|
||||
try {
|
||||
split2[1].toInt()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
permissions.add(
|
||||
Permission(
|
||||
permissionType,
|
||||
kind,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissions.removeIf { it.kind == null && (it.type == "sign_event" || it.type == "nip") }
|
||||
permissions.removeIf { it.type == "nip" && (it.kind == null || !it.kind.containsNip()) }
|
||||
|
||||
onReady(
|
||||
IntentData(
|
||||
"ack",
|
||||
@@ -829,7 +871,7 @@ object IntentUtils {
|
||||
null,
|
||||
CompressionType.NONE,
|
||||
ReturnType.EVENT,
|
||||
listOf(),
|
||||
permissions,
|
||||
"",
|
||||
mutableStateOf(true),
|
||||
mutableStateOf(false),
|
||||
@@ -838,7 +880,7 @@ object IntentUtils {
|
||||
"connect",
|
||||
arrayOf(pubKey),
|
||||
pubKey,
|
||||
relays,
|
||||
relays.ifEmpty { NostrSigner.getInstance().getSavedRelays().toList() },
|
||||
"",
|
||||
account.keyPair.pubKey.toNpub(),
|
||||
EncryptionType.NIP04,
|
||||
@@ -850,6 +892,15 @@ object IntentUtils {
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("nostrconnect", e.message, e)
|
||||
NostrSigner.getInstance().getDatabase(account.keyPair.pubKey.toNpub()).applicationDao().insertLog(
|
||||
LogEntity(
|
||||
0,
|
||||
"nostrconnect",
|
||||
"nostrconnect",
|
||||
e.message ?: "",
|
||||
System.currentTimeMillis(),
|
||||
),
|
||||
)
|
||||
onReady(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import android.util.Log
|
||||
import com.greenart7c3.nostrsigner.checkNotInMainThread
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class Nip11Retriever {
|
||||
enum class ErrorCode {
|
||||
FAIL_TO_ASSEMBLE_URL,
|
||||
FAIL_TO_REACH_SERVER,
|
||||
FAIL_TO_PARSE_RESULT,
|
||||
FAIL_WITH_HTTP_STATUS,
|
||||
}
|
||||
|
||||
suspend fun loadRelayInfo(
|
||||
url: String,
|
||||
dirtyUrl: String,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (String, ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
try {
|
||||
val request: Request =
|
||||
Request
|
||||
.Builder()
|
||||
.header("Accept", "application/nostr+json")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
HttpClientManager
|
||||
.getHttpClientForUrl(dirtyUrl)
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
response.use {
|
||||
val body = it.body.string()
|
||||
try {
|
||||
if (it.isSuccessful) {
|
||||
onInfo(Nip11RelayInformation.fromJson(body))
|
||||
} else {
|
||||
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(
|
||||
"RelayInfoFail",
|
||||
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
|
||||
e,
|
||||
)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import android.util.Log
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.checkNotInMainThread
|
||||
import com.greenart7c3.nostrsigner.database.LogEntity
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.ui.NotificationType
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.NostrDataSource
|
||||
@@ -36,9 +36,14 @@ import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NotificationDataSource : NostrDataSource("AccountData") {
|
||||
private val eventNotificationConsumer = EventNotificationConsumer(NostrSigner.getInstance())
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val clientListener =
|
||||
object : Client.Listener {
|
||||
@@ -136,8 +141,19 @@ object NotificationDataSource : NostrDataSource("AccountData") {
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(this.javaClass.simpleName, "${this.javaClass.simpleName} Subscribe")
|
||||
Client.subscribe(clientListener)
|
||||
scope.launch {
|
||||
LocalPreferences.loadSettingsFromEncryptedStorage()
|
||||
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
Client.subscribe(clientListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
if (!Client.isSubscribed(clientListener)) {
|
||||
Client.subscribe(clientListener)
|
||||
}
|
||||
super.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
|
||||
@@ -27,13 +27,14 @@ import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.database.LogEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import java.net.URL
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
@@ -61,13 +62,44 @@ class RegisterAccounts(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSupportedUrl(url: String): Boolean {
|
||||
return url.isNotBlank() &&
|
||||
!url.contains("brb.io") && // no broken relays
|
||||
!url.contains("echo.websocket.org") && // test relay
|
||||
!url.contains("127.0") && // no local relays
|
||||
!url.contains("umbrel.local") && // no local relays
|
||||
!url.contains("192.168.") && // no local relays
|
||||
!url.contains(".onion") && // we are not running on Tor
|
||||
!url.contains("https://") && // not a websocket
|
||||
!url.contains("http://") && // not a websocket
|
||||
!url.contains("www://") && // not a websocket
|
||||
!url.contains("https//") && // not a websocket
|
||||
!url.contains("http//") && // not a websocket
|
||||
!url.contains("www//") && // not a websocket
|
||||
!url.contains("npub1") && // does not allow custom uris
|
||||
!url.contains("was://") && // common mispellings
|
||||
!url.contains("ws://umbrel:") && // local domain
|
||||
!url.contains("\t") && // tab is not allowed
|
||||
!url.contains(" ") && // space is not allowed
|
||||
isValidUrl(url)
|
||||
}
|
||||
|
||||
private fun isValidUrl(url: String): Boolean {
|
||||
try {
|
||||
URL(url.replace("wss://", "https://").replace("ws://", "http://"))
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// creates proof that it controls all accounts
|
||||
private fun signEventsToProveControlOfAccounts(
|
||||
accounts: List<AccountInfo>,
|
||||
notificationToken: String,
|
||||
onReady: (List<RelayAuthEvent>) -> Unit,
|
||||
) {
|
||||
val readyToSend: MutableList<Pair<Account, String>> = mutableListOf()
|
||||
val readyToSend: MutableSet<Pair<Account, String>> = mutableSetOf()
|
||||
accounts.forEach {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(NostrSigner.getInstance(), it.npub)
|
||||
if (acc != null) {
|
||||
@@ -77,20 +109,32 @@ class RegisterAccounts(
|
||||
).applicationDao().getAll(acc.keyPair.pubKey.toHexKey())
|
||||
permissions.forEach { permission ->
|
||||
permission.relays.forEach { relay ->
|
||||
if (relay.url.isNotBlank()) {
|
||||
if (isSupportedUrl(relay.url)) {
|
||||
readyToSend.add(
|
||||
Pair(acc, relay.url),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
LocalPreferences.loadSettingsFromEncryptedStorage()
|
||||
}
|
||||
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
|
||||
defaultRelays.forEach { relay ->
|
||||
if (isSupportedUrl(relay.url)) {
|
||||
readyToSend.add(
|
||||
Pair(acc, relay.url),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
|
||||
recursiveAuthCreation(
|
||||
notificationToken,
|
||||
readyToSend,
|
||||
readyToSend.toList(),
|
||||
listOfAuthEvents,
|
||||
onReady,
|
||||
)
|
||||
@@ -144,7 +188,6 @@ class RegisterAccounts(
|
||||
),
|
||||
)
|
||||
}
|
||||
AmberListenerSingleton.accountStateViewModel?.toast("Push server", "Failed to register with push server")
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -18,6 +20,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -26,7 +29,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -74,71 +76,106 @@ fun AccountScreen(
|
||||
MainLoginPage(accountStateViewModel)
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
var intentData by remember {
|
||||
mutableStateOf<IntentData?>(null)
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var isNotificationsConfigured by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
LaunchedEffect(intent, intents) {
|
||||
IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) {
|
||||
intentData = it
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
isNotificationsConfigured = LocalPreferences.isNotificationTypeConfigured()
|
||||
isLoading = false
|
||||
}
|
||||
val data =
|
||||
intents.firstOrNull {
|
||||
it.currentAccount.isNotBlank()
|
||||
}
|
||||
}
|
||||
|
||||
data?.bunkerRequest?.let {
|
||||
if (it.currentAccount.isNotBlank()) {
|
||||
if (LocalPreferences.currentAccount(context) != it.currentAccount) {
|
||||
accountStateViewModel.switchUser(it.currentAccount, null)
|
||||
if (isLoading) {
|
||||
Scaffold { innerPadding ->
|
||||
CenterCircularProgressIndicator(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (isNotificationsConfigured) {
|
||||
var intentData by remember {
|
||||
mutableStateOf<IntentData?>(null)
|
||||
}
|
||||
LaunchedEffect(intent, intents) {
|
||||
IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) {
|
||||
intentData = it
|
||||
}
|
||||
val data =
|
||||
intents.firstOrNull {
|
||||
it.currentAccount.isNotBlank()
|
||||
}
|
||||
|
||||
data?.bunkerRequest?.let {
|
||||
if (it.currentAccount.isNotBlank()) {
|
||||
if (LocalPreferences.currentAccount(context) != it.currentAccount) {
|
||||
accountStateViewModel.switchUser(it.currentAccount, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intentData != null) {
|
||||
if (intents.none { item -> item.id == intentData!!.id }) {
|
||||
val oldIntents = intents.toMutableList()
|
||||
oldIntents.add(intentData!!)
|
||||
flow.value = oldIntents
|
||||
}
|
||||
}
|
||||
|
||||
val newIntents =
|
||||
intents.ifEmpty {
|
||||
if (intentData == null) {
|
||||
listOf()
|
||||
} else {
|
||||
listOf(intentData)
|
||||
if (intentData != null) {
|
||||
if (intents.none { item -> item.id == intentData!!.id }) {
|
||||
val oldIntents = intents.toMutableList()
|
||||
oldIntents.add(intentData!!)
|
||||
flow.value = oldIntents
|
||||
}
|
||||
}
|
||||
}.mapNotNull { it }
|
||||
val database = NostrSigner.getInstance().getDatabase(state.account.keyPair.pubKey.toNpub())
|
||||
val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SideEffect {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
PushNotificationUtils.accountState = accountStateViewModel
|
||||
try {
|
||||
NostrSigner.getInstance().applicationContext.startForegroundService(
|
||||
Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java),
|
||||
val newIntents =
|
||||
intents.ifEmpty {
|
||||
if (intentData == null) {
|
||||
listOf()
|
||||
} else {
|
||||
listOf(intentData)
|
||||
}
|
||||
}.mapNotNull { it }
|
||||
val database = NostrSigner.getInstance().getDatabase(state.account.keyPair.pubKey.toNpub())
|
||||
val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route)
|
||||
|
||||
SideEffect {
|
||||
NostrSigner.getInstance().applicationIOScope.launch(Dispatchers.IO) {
|
||||
PushNotificationUtils.accountState = accountStateViewModel
|
||||
try {
|
||||
NostrSigner.getInstance().applicationContext.startForegroundService(
|
||||
Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.d("NostrSigner", "Failed to start ConnectivityService", e)
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
|
||||
NostrSigner.getInstance().checkForNewRelays()
|
||||
NotificationDataSource.start()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
|
||||
|
||||
DisplayErrorMessages(accountStateViewModel)
|
||||
MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database)
|
||||
} else {
|
||||
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
|
||||
DisplayErrorMessages(accountStateViewModel)
|
||||
Scaffold { innerPadding ->
|
||||
NotificationTypeScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
onDone = {
|
||||
isNotificationsConfigured = true
|
||||
},
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.d("NostrSigner", "Failed to start ConnectivityService", e)
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
|
||||
NostrSigner.getInstance().checkForNewRelays()
|
||||
NotificationDataSource.start()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
|
||||
|
||||
DisplayErrorMessages(accountStateViewModel)
|
||||
MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ fun EditPermission(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
permissions.addAll(database.applicationDao().getAllByKey(selectedPackage).sortedBy { "${it.type}-${it.kind}" })
|
||||
applicationData = database.applicationDao().getByKey(selectedPackage)!!.application
|
||||
checked = applicationData.useSecret
|
||||
@@ -122,6 +122,7 @@ fun EditPermission(
|
||||
if (editRelaysDialog) {
|
||||
EditRelaysDialog(
|
||||
applicationData = applicationData,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
onClose = {
|
||||
editRelaysDialog = false
|
||||
},
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
|
||||
import java.io.IOException
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
|
||||
@Composable
|
||||
fun LanguageScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
account: Account,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val languageEntries = remember { context.getLangPreferenceDropdownEntries() }
|
||||
val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() }
|
||||
val languageIndex = getLanguageIndex(languageEntries, account.language)
|
||||
|
||||
Surface(
|
||||
modifier.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.language,
|
||||
R.string.language_description,
|
||||
languageList,
|
||||
languageIndex,
|
||||
) {
|
||||
account.language = languageEntries[languageList[it].title]
|
||||
LocalPreferences.saveToEncryptedStorage(context, account)
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(account.language),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getLocaleListFromXml(): LocaleListCompat {
|
||||
val tagsList = mutableListOf<CharSequence>()
|
||||
try {
|
||||
val xpp: XmlPullParser = resources.getXml(R.xml.locales_config)
|
||||
while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (xpp.eventType == XmlPullParser.START_TAG) {
|
||||
if (xpp.name == "locale") {
|
||||
tagsList.add(xpp.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
xpp.next()
|
||||
}
|
||||
} catch (e: XmlPullParserException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
|
||||
}
|
||||
|
||||
fun Context.getLangPreferenceDropdownEntries(): ImmutableMap<String, String> {
|
||||
val localeList = getLocaleListFromXml()
|
||||
val map = mutableMapOf<String, String>()
|
||||
|
||||
for (a in 0 until localeList.size()) {
|
||||
localeList[a].let {
|
||||
map.put(
|
||||
it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() },
|
||||
it.toLanguageTag(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return map.toImmutableMap()
|
||||
}
|
||||
|
||||
fun getLanguageIndex(
|
||||
languageEntries: ImmutableMap<String, String>,
|
||||
selectedLanguage: String?,
|
||||
): Int {
|
||||
var languageIndex: Int
|
||||
languageIndex =
|
||||
if (selectedLanguage != null) {
|
||||
languageEntries.values.toTypedArray().indexOf(selectedLanguage)
|
||||
} else {
|
||||
languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
|
||||
}
|
||||
if (languageIndex == -1) {
|
||||
languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language)
|
||||
}
|
||||
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf("en")
|
||||
return languageIndex
|
||||
}
|
||||
@@ -132,9 +132,9 @@ fun MainPage(
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ElevatedButton(
|
||||
onClick = {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LogsScreen(
|
||||
account: Account,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Surface(modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
val logsFlow = NostrSigner.getInstance().getDatabase(account.keyPair.pubKey.toNpub()).applicationDao().getLogs()
|
||||
val logs = logsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
LazyColumn(
|
||||
Modifier.weight(1f),
|
||||
) {
|
||||
items(logs.value.size) { index ->
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(6.dp),
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val log = logs.value[index]
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("Date: ")
|
||||
}
|
||||
append(TimeUtils.convertLongToDateTime(log.time))
|
||||
},
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("URL: ")
|
||||
}
|
||||
append(log.url)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("Type: ")
|
||||
}
|
||||
append(log.type)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("Message: ")
|
||||
}
|
||||
append(log.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrSigner.getInstance().getDatabase(account.keyPair.pubKey.toNpub()).applicationDao().clearLogs()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.clear_logs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,7 @@ import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -31,25 +28,15 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Hub
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.RichTooltip
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -99,16 +86,16 @@ import com.greenart7c3.nostrsigner.service.EventNotificationConsumer
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
|
||||
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ActiveRelaysDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupScreen
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ActiveRelaysScreen
|
||||
import com.greenart7c3.nostrsigner.ui.actions.DefaultRelaysScreen
|
||||
import com.greenart7c3.nostrsigner.ui.components.BackButtonScaffold
|
||||
import com.greenart7c3.nostrsigner.ui.components.MainScaffold
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Base64
|
||||
import java.util.UUID
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -137,11 +124,7 @@ fun sendResult(
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
|
||||
val savedApplication = database.applicationDao().getByKey(key)
|
||||
val relays = if (savedApplication != null) {
|
||||
savedApplication.application.relays.ifEmpty { defaultRelays }
|
||||
} else {
|
||||
intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays
|
||||
}
|
||||
val relays = savedApplication?.application?.relays?.ifEmpty { defaultRelays } ?: (intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays)
|
||||
if (intentData.bunkerRequest != null) {
|
||||
NostrSigner.getInstance().checkForNewRelays(
|
||||
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
|
||||
@@ -392,6 +375,61 @@ fun PermissionsFloatingActionButton(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(end = 10.dp)
|
||||
.clickable {
|
||||
dialogOpen = true
|
||||
expanded = false
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
RichTooltip {
|
||||
Text(stringResource(R.string.paste_from_clipboard))
|
||||
}
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
clipboardManager.getText()?.let {
|
||||
if (it.text.isBlank()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.invalid_nostr_connect_uri),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
return@let
|
||||
}
|
||||
if (!it.text.startsWith("nostrconnect://")) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.invalid_nostr_connect_uri),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
return@let
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(it.text)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
context.getAppCompatActivity()?.startActivity(intent)
|
||||
accountStateViewModel.switchUser(account.keyPair.pubKey.toNpub(), Route.Home.route)
|
||||
}
|
||||
expanded = false
|
||||
},
|
||||
modifier = Modifier.size(35.dp),
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentPaste,
|
||||
contentDescription = stringResource(R.string.paste_from_clipboard),
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.padding(end = 10.dp)
|
||||
@@ -486,7 +524,9 @@ private suspend fun askNotificationPermission(
|
||||
onShouldShowRequestPermissionRationale: () -> Unit,
|
||||
) {
|
||||
if (ContextCompat.checkSelfPermission(context, "android.permission.POST_NOTIFICATIONS") == PackageManager.PERMISSION_GRANTED) {
|
||||
initNotifications(context)
|
||||
initNotifications(
|
||||
context = context,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -512,7 +552,6 @@ private suspend fun initNotifications(context: Context) {
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
account: Account,
|
||||
@@ -526,17 +565,7 @@ fun MainScreen(
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val destinationRoute = navBackStackEntry?.destination?.route ?: ""
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.Settings)
|
||||
var shouldShowBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
confirmValueChange = { it != SheetValue.PartiallyExpanded },
|
||||
skipPartiallyExpanded = true,
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val requestPermissionLauncher =
|
||||
@@ -545,7 +574,9 @@ fun MainScreen(
|
||||
) { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
initNotifications(context)
|
||||
initNotifications(
|
||||
context = context,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (LocalPreferences.shouldShowRationale(context) == null) {
|
||||
@@ -560,11 +591,12 @@ fun MainScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
askNotificationPermission(
|
||||
context,
|
||||
requestPermissionLauncher,
|
||||
) {
|
||||
showDialog = true
|
||||
}
|
||||
context = context,
|
||||
requestPermissionLauncher = requestPermissionLauncher,
|
||||
onShouldShowRequestPermissionRationale = {
|
||||
showDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,173 +635,37 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (destinationRoute == "Permissions" && BuildConfig.FLAVOR != "offline") {
|
||||
PermissionsFloatingActionButton(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
) {
|
||||
val secret = UUID.randomUUID().toString().substring(0, 6)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val application =
|
||||
ApplicationEntity(
|
||||
secret,
|
||||
"",
|
||||
NostrSigner.getInstance().settings.defaultRelays,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
false,
|
||||
secret,
|
||||
false,
|
||||
account.signPolicy,
|
||||
)
|
||||
var localRoute by remember { mutableStateOf(route.value ?: Route.Home.route) }
|
||||
|
||||
database.applicationDao().insertApplication(
|
||||
application,
|
||||
)
|
||||
val relayString = NostrSigner.getInstance().settings.defaultRelays.joinToString(separator = "&") { "relay=${it.url}" }
|
||||
val bunkerUrl = "bunker://${account.keyPair.pubKey.toHexKey()}?$relayString"
|
||||
clipboardManager.setText(AnnotatedString(bunkerUrl))
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigate("Permission/$secret")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
if (shouldShowBottomSheet) {
|
||||
AccountsBottomSheet(
|
||||
sheetState = sheetState,
|
||||
account = account,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
onClose = {
|
||||
scope.launch {
|
||||
shouldShowBottomSheet = false
|
||||
sheetState.hide()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
actions = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
var showDialogRelay by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDialogRelay) {
|
||||
ActiveRelaysDialog(
|
||||
onClose = {
|
||||
showDialogRelay = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(40.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
showDialogRelay = true
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Hub,
|
||||
contentDescription = "Relays",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
val name = LocalPreferences.getAccountName(context, account.keyPair.pubKey.toNpub())
|
||||
Row(
|
||||
Modifier
|
||||
.border(
|
||||
border = ButtonDefaults.outlinedButtonBorder,
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
.padding(8.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
shouldShowBottomSheet = true
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(name.ifBlank { account.keyPair.pubKey.toNpub().toShortenHex() })
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
if (!destinationRoute.contains("Permission/")) {
|
||||
NavigationBar(tonalElevation = 0.dp) {
|
||||
items.forEach {
|
||||
val selected = destinationRoute == it.route
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(it.route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
if (selected) it.selectedIcon else it.icon,
|
||||
it.route,
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(it.title)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
var localRoute by remember { mutableStateOf(route.value ?: Route.Home.route) }
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
LaunchedEffect(Unit, route.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
if (route.value != null) {
|
||||
localRoute = route.value!!
|
||||
route.value = null
|
||||
}
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
LaunchedEffect(Unit, route.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
if (route.value != null) {
|
||||
localRoute = route.value!!
|
||||
route.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = localRoute,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
||||
) {
|
||||
composable(
|
||||
Route.Home.route,
|
||||
content = {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = localRoute,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
||||
) {
|
||||
composable(
|
||||
Route.Home.route,
|
||||
content = {
|
||||
MainScaffold(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
database,
|
||||
navController,
|
||||
destinationRoute,
|
||||
false,
|
||||
) { padding ->
|
||||
HomeScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -780,12 +676,21 @@ fun MainScreen(
|
||||
account,
|
||||
database,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permissions.route,
|
||||
content = {
|
||||
composable(
|
||||
Route.Permissions.route,
|
||||
content = {
|
||||
MainScaffold(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
database,
|
||||
navController,
|
||||
destinationRoute,
|
||||
true,
|
||||
) { padding ->
|
||||
PermissionsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -795,26 +700,56 @@ fun MainScreen(
|
||||
navController = navController,
|
||||
database = database,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Settings.route,
|
||||
content = {
|
||||
composable(
|
||||
Route.Settings.route,
|
||||
content = {
|
||||
MainScaffold(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
database,
|
||||
navController,
|
||||
destinationRoute,
|
||||
false,
|
||||
) { padding ->
|
||||
SettingsScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
accountStateViewModel,
|
||||
account,
|
||||
navController,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permission.route,
|
||||
arguments = listOf(navArgument("packageName") { type = NavType.StringType }),
|
||||
content = {
|
||||
composable(
|
||||
Route.AccountBackup.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
AccountBackupScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permission.route,
|
||||
arguments = listOf(navArgument("packageName") { type = NavType.StringType }),
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
EditPermission(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -826,8 +761,130 @@ fun MainScreen(
|
||||
navController = navController,
|
||||
database = database,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Logs.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
LogsScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.ActiveRelays.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
ActiveRelaysScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Language.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
LanguageScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.NotificationType.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
NotificationTypeScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onDone = {
|
||||
navController.navigateUp()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.DefaultRelays.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
DefaultRelaysScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
navController = navController,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.SignPolicy.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
SignPolicySettingsScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
account = account,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Security.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
SecurityScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
|
||||
import com.greenart7c3.nostrsigner.service.ConnectivityService
|
||||
import com.greenart7c3.nostrsigner.service.NotificationDataSource
|
||||
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.ammolite.relays.RelayPool
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NotificationTypeScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
if (!LocalPreferences.isNotificationTypeConfigured()) {
|
||||
AmberListenerSingleton.accountStateViewModel?.toast(
|
||||
context.getString(R.string.notification_settings),
|
||||
context.getString(R.string.please_configure_your_notification_settings_before_continuing),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var notificationItemsIndex by remember {
|
||||
mutableIntStateOf(NostrSigner.getInstance().settings.notificationType.screenCode)
|
||||
}
|
||||
val notificationItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(
|
||||
stringResource(NotificationType.PUSH.resourceId),
|
||||
stringResource(R.string.push_notifications_explainer),
|
||||
),
|
||||
TitleExplainer(
|
||||
stringResource(NotificationType.DIRECT.resourceId),
|
||||
stringResource(R.string.direct_notifications_explainer),
|
||||
),
|
||||
)
|
||||
Surface(
|
||||
modifier.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Column(
|
||||
Modifier.weight(1f),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.notification_type,
|
||||
R.string.select_the_type_of_notification_you_want_to_receive,
|
||||
notificationItems,
|
||||
notificationItemsIndex,
|
||||
) {
|
||||
notificationItemsIndex = it
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "free") {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
PushNotificationSettingsRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
PostButton(
|
||||
isActive = true,
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
|
||||
notificationType = parseNotificationType(notificationItemsIndex),
|
||||
)
|
||||
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
|
||||
PushNotificationUtils.hasInit = false
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
|
||||
if (notificationItemsIndex == 0) {
|
||||
NotificationDataSource.stopSync()
|
||||
RelayPool.disconnect()
|
||||
} else {
|
||||
NostrSigner.getInstance().checkForNewRelays()
|
||||
NotificationDataSource.start()
|
||||
}
|
||||
NostrSigner.getInstance().applicationContext.startForegroundService(
|
||||
Intent(
|
||||
NostrSigner.getInstance().applicationContext,
|
||||
ConnectivityService::class.java,
|
||||
),
|
||||
)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SecurityScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavController,
|
||||
) {
|
||||
val biometricItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringResource(BiometricsTimeType.EVERY_TIME.resourceId)),
|
||||
TitleExplainer(stringResource(BiometricsTimeType.ONE_MINUTE.resourceId)),
|
||||
TitleExplainer(stringResource(BiometricsTimeType.FIVE_MINUTES.resourceId)),
|
||||
TitleExplainer(stringResource(BiometricsTimeType.TEN_MINUTES.resourceId)),
|
||||
)
|
||||
var enableBiometrics by remember { mutableStateOf(NostrSigner.getInstance().settings.useAuth) }
|
||||
var biometricsIndex by remember {
|
||||
mutableIntStateOf(NostrSigner.getInstance().settings.biometricsTimeType.screenCode)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
Surface(modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Column(
|
||||
Modifier.weight(1f),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable {
|
||||
enableBiometrics = !enableBiometrics
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.enable_biometrics),
|
||||
)
|
||||
Switch(
|
||||
checked = enableBiometrics,
|
||||
onCheckedChange = {
|
||||
enableBiometrics = !enableBiometrics
|
||||
},
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.when_to_ask,
|
||||
R.string.when_to_ask,
|
||||
biometricItems,
|
||||
biometricsIndex,
|
||||
) {
|
||||
biometricsIndex = it
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
PostButton(isActive = true) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
|
||||
useAuth = enableBiometrics,
|
||||
biometricsTimeType = parseBiometricsTimeType(biometricsIndex),
|
||||
)
|
||||
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,16 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Draw
|
||||
@@ -31,17 +22,11 @@ import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.SurroundSound
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -51,556 +36,45 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.Biometrics
|
||||
import com.greenart7c3.nostrsigner.service.ConnectivityService
|
||||
import com.greenart7c3.nostrsigner.service.NotificationDataSource
|
||||
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ConnectOrbotDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.EditDefaultRelaysDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.LogoutDialog
|
||||
import com.greenart7c3.nostrsigner.ui.components.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.HyperlinkText
|
||||
import com.greenart7c3.nostrsigner.ui.components.IconRow
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.TextSpinner
|
||||
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.ammolite.relays.RelayPool
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import java.io.IOException
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
|
||||
fun Context.getLocaleListFromXml(): LocaleListCompat {
|
||||
val tagsList = mutableListOf<CharSequence>()
|
||||
try {
|
||||
val xpp: XmlPullParser = resources.getXml(R.xml.locales_config)
|
||||
while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (xpp.eventType == XmlPullParser.START_TAG) {
|
||||
if (xpp.name == "locale") {
|
||||
tagsList.add(xpp.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
xpp.next()
|
||||
}
|
||||
} catch (e: XmlPullParserException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
|
||||
}
|
||||
|
||||
fun Context.getLangPreferenceDropdownEntries(): ImmutableMap<String, String> {
|
||||
val localeList = getLocaleListFromXml()
|
||||
val map = mutableMapOf<String, String>()
|
||||
|
||||
for (a in 0 until localeList.size()) {
|
||||
localeList[a].let {
|
||||
map.put(
|
||||
it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() },
|
||||
it.toLanguageTag(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return map.toImmutableMap()
|
||||
}
|
||||
|
||||
fun getLanguageIndex(
|
||||
languageEntries: ImmutableMap<String, String>,
|
||||
selectedLanguage: String?,
|
||||
): Int {
|
||||
var languageIndex: Int
|
||||
languageIndex =
|
||||
if (selectedLanguage != null) {
|
||||
languageEntries.values.toTypedArray().indexOf(selectedLanguage)
|
||||
} else {
|
||||
languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
|
||||
}
|
||||
if (languageIndex == -1) {
|
||||
languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language)
|
||||
}
|
||||
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf("en")
|
||||
return languageIndex
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
modifier: Modifier,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
account: Account,
|
||||
navController: NavController,
|
||||
) {
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
var checked by remember { mutableStateOf(account.useProxy) }
|
||||
var disconnectTorDialog by remember { mutableStateOf(false) }
|
||||
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf(account.proxyPort.toString()) }
|
||||
val languageEntries = remember { context.getLangPreferenceDropdownEntries() }
|
||||
val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() }
|
||||
val languageIndex = getLanguageIndex(languageEntries, account.language)
|
||||
var languageDialog by remember { mutableStateOf(false) }
|
||||
var logDialog by remember { mutableStateOf(false) }
|
||||
var relayDialog by remember { mutableStateOf(false) }
|
||||
var allowNewConnections by remember { mutableStateOf(account.allowNewConnections) }
|
||||
var signatureDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val notificationItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringResource(NotificationType.PUSH.resourceId)),
|
||||
TitleExplainer(stringResource(NotificationType.DIRECT.resourceId)),
|
||||
)
|
||||
|
||||
val biometricItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringResource(BiometricsTimeType.EVERY_TIME.resourceId)),
|
||||
TitleExplainer(stringResource(BiometricsTimeType.ONE_MINUTE.resourceId)),
|
||||
TitleExplainer(stringResource(BiometricsTimeType.FIVE_MINUTES.resourceId)),
|
||||
TitleExplainer(stringResource(BiometricsTimeType.TEN_MINUTES.resourceId)),
|
||||
)
|
||||
|
||||
var notificationTypeDialog by remember { mutableStateOf(false) }
|
||||
var securityDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (relayDialog) {
|
||||
EditDefaultRelaysDialog(
|
||||
onClose = {
|
||||
relayDialog = false
|
||||
},
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (it.isNotEmpty()) {
|
||||
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
|
||||
defaultRelays = it,
|
||||
)
|
||||
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
|
||||
}
|
||||
relayDialog = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (signatureDialog) {
|
||||
val radioOptions = listOf(
|
||||
TitleExplainer(
|
||||
title = stringResource(R.string.sign_policy_basic),
|
||||
explainer = stringResource(R.string.sign_policy_basic_explainer),
|
||||
),
|
||||
TitleExplainer(
|
||||
title = stringResource(R.string.sign_policy_manual),
|
||||
explainer = stringResource(R.string.sign_policy_manual_explainer),
|
||||
),
|
||||
)
|
||||
var selectedOption by remember { mutableIntStateOf(account.signPolicy) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
signatureDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
CloseButton {
|
||||
signatureDialog = false
|
||||
}
|
||||
|
||||
PostButton(isActive = true) {
|
||||
signatureDialog = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
account.signPolicy = selectedOption
|
||||
LocalPreferences.saveToEncryptedStorage(context, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally,
|
||||
) {
|
||||
radioOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selectedOption == index,
|
||||
onClick = {
|
||||
selectedOption = index
|
||||
},
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (selectedOption == index) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedOption == index,
|
||||
onClick = {
|
||||
selectedOption = index
|
||||
},
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = option.title,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
option.explainer?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
logDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
CloseButton {
|
||||
logDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
val logsFlow = NostrSigner.getInstance().getDatabase(account.keyPair.pubKey.toNpub()).applicationDao().getLogs()
|
||||
val logs = logsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
LazyColumn(
|
||||
Modifier.weight(1f),
|
||||
) {
|
||||
items(logs.value.size) { index ->
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(6.dp),
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val log = logs.value[index]
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("Date: ")
|
||||
}
|
||||
append(TimeUtils.convertLongToDateTime(log.time))
|
||||
},
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("URL: ")
|
||||
}
|
||||
append(log.url)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("Type: ")
|
||||
}
|
||||
append(log.type)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("Message: ")
|
||||
}
|
||||
append(log.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrSigner.getInstance().getDatabase(account.keyPair.pubKey.toNpub()).applicationDao().clearLogs()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.clear_logs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationTypeDialog) {
|
||||
var notificationItemsIndex by remember {
|
||||
mutableIntStateOf(NostrSigner.getInstance().settings.notificationType.screenCode)
|
||||
}
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
notificationTypeDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
CloseButton {
|
||||
notificationTypeDialog = false
|
||||
}
|
||||
|
||||
PostButton(
|
||||
isActive = true,
|
||||
) {
|
||||
notificationTypeDialog = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
|
||||
notificationType = parseNotificationType(notificationItemsIndex),
|
||||
)
|
||||
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
|
||||
PushNotificationUtils.hasInit = false
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
|
||||
if (notificationItemsIndex == 0) {
|
||||
NotificationDataSource.stopSync()
|
||||
RelayPool.disconnect()
|
||||
} else {
|
||||
NostrSigner.getInstance().checkForNewRelays()
|
||||
NotificationDataSource.start()
|
||||
}
|
||||
NostrSigner.getInstance().applicationContext.startForegroundService(
|
||||
Intent(
|
||||
NostrSigner.getInstance().applicationContext,
|
||||
ConnectivityService::class.java,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.notification_type,
|
||||
R.string.select_the_type_of_notification_you_want_to_receive,
|
||||
notificationItems,
|
||||
notificationItemsIndex,
|
||||
) {
|
||||
notificationItemsIndex = it
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "free") {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
PushNotificationSettingsRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (languageDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
languageDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
CloseButton {
|
||||
languageDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.language,
|
||||
R.string.language_description,
|
||||
languageList,
|
||||
languageIndex,
|
||||
) {
|
||||
account.language = languageEntries[languageList[it].title]
|
||||
LocalPreferences.saveToEncryptedStorage(context, account)
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(account.language),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (securityDialog) {
|
||||
var biometricsIndex by remember {
|
||||
mutableIntStateOf(NostrSigner.getInstance().settings.biometricsTimeType.screenCode)
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
securityDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
var enableBiometrics by remember { mutableStateOf(NostrSigner.getInstance().settings.useAuth) }
|
||||
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
CloseButton {
|
||||
securityDialog = false
|
||||
}
|
||||
PostButton(isActive = true) {
|
||||
securityDialog = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
|
||||
useAuth = enableBiometrics,
|
||||
biometricsTimeType = parseBiometricsTimeType(biometricsIndex),
|
||||
)
|
||||
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable {
|
||||
enableBiometrics = !enableBiometrics
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.enable_biometrics),
|
||||
)
|
||||
Switch(
|
||||
checked = enableBiometrics,
|
||||
onCheckedChange = {
|
||||
enableBiometrics = !enableBiometrics
|
||||
},
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.when_to_ask,
|
||||
R.string.when_to_ask,
|
||||
biometricItems,
|
||||
biometricsIndex,
|
||||
) {
|
||||
biometricsIndex = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logoutDialog) {
|
||||
LogoutDialog(
|
||||
@@ -628,7 +102,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Security,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
securityDialog = true
|
||||
navController.navigate(Route.Security.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -643,7 +117,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Key,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
backupDialogOpen = true
|
||||
navController.navigate(Route.AccountBackup.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -657,7 +131,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Language,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
languageDialog = true
|
||||
navController.navigate(Route.Language.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -673,7 +147,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Notifications,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
notificationTypeDialog = true
|
||||
navController.navigate(Route.NotificationType.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -713,7 +187,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Hub,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
relayDialog = true
|
||||
navController.navigate(Route.DefaultRelays.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -743,7 +217,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.FilterList,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
logDialog = true
|
||||
navController.navigate(Route.Logs.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -757,7 +231,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Draw,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
signatureDialog = true
|
||||
navController.navigate(Route.SignPolicy.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -832,10 +306,6 @@ fun SettingsScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SignPolicySettingsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
account: Account,
|
||||
navController: NavController,
|
||||
) {
|
||||
val radioOptions = listOf(
|
||||
TitleExplainer(
|
||||
title = stringResource(R.string.sign_policy_basic),
|
||||
explainer = stringResource(R.string.sign_policy_basic_explainer),
|
||||
),
|
||||
TitleExplainer(
|
||||
title = stringResource(R.string.sign_policy_manual),
|
||||
explainer = stringResource(R.string.sign_policy_manual_explainer),
|
||||
),
|
||||
)
|
||||
var selectedOption by remember { mutableIntStateOf(account.signPolicy) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
Surface(modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
itemsIndexed(radioOptions) { index, option ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selectedOption == index,
|
||||
onClick = {
|
||||
selectedOption = index
|
||||
},
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (selectedOption == index) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedOption == index,
|
||||
onClick = {
|
||||
selectedOption = index
|
||||
},
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = option.title,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
option.explainer?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
PostButton(isActive = true) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
account.signPolicy = selectedOption
|
||||
LocalPreferences.saveToEncryptedStorage(context, account)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
@@ -86,9 +85,9 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun AccountBackupDialog(
|
||||
fun AccountBackupScreen(
|
||||
modifier: Modifier,
|
||||
account: Account,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
var showSeedWords by remember { mutableStateOf(false) }
|
||||
if (showSeedWords) {
|
||||
@@ -122,189 +121,173 @@ fun AccountBackupDialog(
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
Surface(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
.padding(horizontal = 30.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
val content = stringResource(R.string.account_backup_tips_md)
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
CloseButton(onCancel = onClose)
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 30.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val content = stringResource(R.string.account_backup_tips_md)
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
if (account.seedWords.isNotEmpty()) {
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
showSeedWords = true
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
if (account.seedWords.isNotEmpty()) {
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
Button(
|
||||
onClick = {
|
||||
authenticate(
|
||||
title = context.getString(R.string.show_seed_words),
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
showSeedWords = true
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
authenticate(
|
||||
title = context.getString(R.string.show_seed_words),
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
showSeedWords = true
|
||||
},
|
||||
onError = { _, message ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.List,
|
||||
contentDescription = stringResource(R.string.show_seed_words),
|
||||
)
|
||||
Text(text = stringResource(R.string.show_seed_words))
|
||||
}
|
||||
}
|
||||
}
|
||||
NSecCopyButton(account)
|
||||
NSecQrButton(account)
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
val content1 = stringResource(R.string.account_backup_tips3_md)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
BasicMarkdown(astNode1)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
val password = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
var showCharsPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { password.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.ncryptsec_password),
|
||||
},
|
||||
onError = { _, message ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
|
||||
Icon(
|
||||
imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription = if (showCharsPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.List,
|
||||
contentDescription = stringResource(R.string.show_seed_words),
|
||||
)
|
||||
Text(text = stringResource(R.string.show_seed_words))
|
||||
}
|
||||
}
|
||||
}
|
||||
NSecCopyButton(account)
|
||||
NSecQrButton(account)
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
val content1 = stringResource(R.string.account_backup_tips3_md)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
BasicMarkdown(astNode1)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
val password = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
var showCharsPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { password.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
)
|
||||
|
||||
if (errorMessage.isNotBlank()) {
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
text = stringResource(R.string.ncryptsec_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
|
||||
Icon(
|
||||
imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription = if (showCharsPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
EncryptNSecCopyButton(account, password)
|
||||
EncryptNSecQRButton(account, password)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
EncryptNSecCopyButton(account, password)
|
||||
EncryptNSecQRButton(account, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.greenart7c3.nostrsigner.ui.actions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -26,10 +27,12 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -49,29 +52,49 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.database.ApplicationEntity
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.Nip11Retriever
|
||||
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
|
||||
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
|
||||
import com.greenart7c3.nostrsigner.ui.components.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
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.RelaySetupInfo
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun EditRelaysDialog(
|
||||
applicationData: ApplicationEntity,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
onClose: () -> Unit,
|
||||
onPost: (SnapshotStateList<RelaySetupInfo>) -> Unit,
|
||||
) {
|
||||
var textFieldRelay by remember {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val textFieldRelay = remember {
|
||||
mutableStateOf(TextFieldValue(""))
|
||||
}
|
||||
val isLoading = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val relays2 =
|
||||
remember {
|
||||
val localRelays = mutableStateListOf<RelaySetupInfo>()
|
||||
@@ -90,147 +113,118 @@ fun EditRelaysDialog(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Row(
|
||||
if (isLoading.value) {
|
||||
CenterCircularProgressIndicator(Modifier)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
CloseButton {
|
||||
onClose()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton {
|
||||
onClose()
|
||||
}
|
||||
PostButton(
|
||||
isActive = !applicationData.isConnected,
|
||||
onPost = {
|
||||
onPost(relays2)
|
||||
},
|
||||
)
|
||||
}
|
||||
PostButton(
|
||||
isActive = !applicationData.isConnected,
|
||||
onPost = {
|
||||
onPost(relays2)
|
||||
},
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
items(relays2.size) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
relays2[it].url,
|
||||
Modifier
|
||||
.weight(0.9f)
|
||||
.padding(8.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
relays2.removeAt(it)
|
||||
},
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
items(relays2.size) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
stringResource(R.string.delete),
|
||||
Text(
|
||||
relays2[it].url,
|
||||
Modifier
|
||||
.weight(0.9f)
|
||||
.padding(8.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
relays2.removeAt(it)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
stringResource(R.string.delete),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
enabled = !applicationData.isConnected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.padding(horizontal = 16.dp),
|
||||
value = textFieldRelay.text,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
onValueChange = {
|
||||
textFieldRelay = TextFieldValue(it)
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
if (applicationData.isConnected) return@KeyboardActions
|
||||
val url = textFieldRelay.text
|
||||
if (url.isNotBlank() && url != "/") {
|
||||
var addedWSS =
|
||||
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
||||
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
|
||||
"ws://$url"
|
||||
} else {
|
||||
"wss://$url"
|
||||
}
|
||||
} else {
|
||||
url
|
||||
}
|
||||
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
|
||||
if (relays2.any { it.url == addedWSS }) {
|
||||
textFieldRelay = TextFieldValue("")
|
||||
return@KeyboardActions
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
enabled = !applicationData.isConnected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.padding(horizontal = 16.dp),
|
||||
value = textFieldRelay.value.text,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
onValueChange = {
|
||||
textFieldRelay.value = TextFieldValue(it)
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
if (applicationData.isConnected) return@KeyboardActions
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
onAddRelay(
|
||||
textFieldRelay,
|
||||
isLoading,
|
||||
relays2,
|
||||
scope,
|
||||
accountStateViewModel,
|
||||
context,
|
||||
)
|
||||
}
|
||||
relays2.add(
|
||||
RelaySetupInfo(
|
||||
addedWSS,
|
||||
read = true,
|
||||
write = true,
|
||||
feedTypes = COMMON_FEED_TYPES,
|
||||
),
|
||||
},
|
||||
),
|
||||
label = {
|
||||
Text("Relay")
|
||||
},
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (applicationData.isConnected) return@IconButton
|
||||
scope.launch(Dispatchers.IO) {
|
||||
onAddRelay(
|
||||
textFieldRelay,
|
||||
isLoading,
|
||||
relays2,
|
||||
scope,
|
||||
accountStateViewModel,
|
||||
context,
|
||||
)
|
||||
textFieldRelay = TextFieldValue("")
|
||||
}
|
||||
},
|
||||
),
|
||||
label = {
|
||||
Text("Relay")
|
||||
},
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (applicationData.isConnected) return@IconButton
|
||||
val url = textFieldRelay.text
|
||||
if (url.isNotBlank() && url != "/") {
|
||||
var addedWSS =
|
||||
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
||||
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
|
||||
"ws://$url"
|
||||
} else {
|
||||
"wss://$url"
|
||||
}
|
||||
} else {
|
||||
url
|
||||
}
|
||||
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
|
||||
if (relays2.any { it.url == addedWSS }) {
|
||||
textFieldRelay = TextFieldValue("")
|
||||
return@IconButton
|
||||
}
|
||||
relays2.add(
|
||||
RelaySetupInfo(
|
||||
addedWSS,
|
||||
read = true,
|
||||
write = true,
|
||||
feedTypes = COMMON_FEED_TYPES,
|
||||
),
|
||||
)
|
||||
textFieldRelay = TextFieldValue("")
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
null,
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,13 +233,19 @@ fun EditRelaysDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditDefaultRelaysDialog(
|
||||
onClose: () -> Unit,
|
||||
onPost: (SnapshotStateList<RelaySetupInfo>) -> Unit,
|
||||
fun DefaultRelaysScreen(
|
||||
modifier: Modifier,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
navController: NavController,
|
||||
) {
|
||||
var textFieldRelay by remember {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val textFieldRelay = remember {
|
||||
mutableStateOf(TextFieldValue(""))
|
||||
}
|
||||
val isLoading = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val relays2 =
|
||||
remember {
|
||||
val localRelays = mutableStateListOf<RelaySetupInfo>()
|
||||
@@ -256,39 +256,101 @@ fun EditDefaultRelaysDialog(
|
||||
}
|
||||
localRelays
|
||||
}
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
if (isLoading.value) {
|
||||
CenterCircularProgressIndicator(Modifier)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.padding(horizontal = 16.dp),
|
||||
value = textFieldRelay.value.text,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
onValueChange = {
|
||||
textFieldRelay.value = TextFieldValue(it)
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
onAddRelay(
|
||||
textFieldRelay,
|
||||
isLoading,
|
||||
relays2,
|
||||
scope,
|
||||
accountStateViewModel,
|
||||
context,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
label = {
|
||||
Text("Relay")
|
||||
},
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
onAddRelay(
|
||||
textFieldRelay,
|
||||
isLoading,
|
||||
relays2,
|
||||
scope,
|
||||
accountStateViewModel,
|
||||
context,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton {
|
||||
onClose()
|
||||
}
|
||||
PostButton(
|
||||
isActive = relays2.isNotEmpty(),
|
||||
onPost = {
|
||||
onPost(relays2)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (relays2.isNotEmpty()) {
|
||||
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
|
||||
defaultRelays = relays2,
|
||||
)
|
||||
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
.weight(1f),
|
||||
) {
|
||||
items(relays2.size) {
|
||||
Row(
|
||||
@@ -317,40 +379,103 @@ fun EditDefaultRelaysDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.padding(horizontal = 16.dp),
|
||||
value = textFieldRelay.text,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
onValueChange = {
|
||||
textFieldRelay = TextFieldValue(it)
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
val url = textFieldRelay.text
|
||||
if (url.isNotBlank() && url != "/") {
|
||||
var addedWSS =
|
||||
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
||||
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
|
||||
"ws://$url"
|
||||
} else {
|
||||
"wss://$url"
|
||||
}
|
||||
} else {
|
||||
url
|
||||
}
|
||||
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
|
||||
if (relays2.any { it.url == addedWSS }) {
|
||||
textFieldRelay = TextFieldValue("")
|
||||
return@KeyboardActions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onAddRelay(
|
||||
textFieldRelay: MutableState<TextFieldValue>,
|
||||
isLoading: MutableState<Boolean>,
|
||||
relays2: SnapshotStateList<RelaySetupInfo>,
|
||||
scope: CoroutineScope,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
context: Context,
|
||||
) {
|
||||
val url = textFieldRelay.value.text
|
||||
if (url.isNotBlank() && url != "/") {
|
||||
isLoading.value = true
|
||||
var addedWSS =
|
||||
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
||||
// TODO: How to identify relays on the local network?
|
||||
val isLocalHost = url.contains("127.0.0.1") || url.contains("localhost")
|
||||
if (url.endsWith(".onion") || url.endsWith(".onion/") || isLocalHost) {
|
||||
"ws://$url"
|
||||
} else {
|
||||
"wss://$url"
|
||||
}
|
||||
} else {
|
||||
url
|
||||
}
|
||||
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
|
||||
if (relays2.any { it.url == addedWSS }) {
|
||||
textFieldRelay.value = TextFieldValue("")
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
val httpsUrl = RelayUrlFormatter.getHttpsUrl(addedWSS)
|
||||
val retriever = Nip11Retriever()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
retriever.loadRelayInfo(
|
||||
httpsUrl,
|
||||
addedWSS,
|
||||
onInfo = { info ->
|
||||
scope.launch(Dispatchers.IO) secondLaunch@{
|
||||
if (info.limitation?.payment_required == true) {
|
||||
accountStateViewModel.toast(
|
||||
context.getString(R.string.relay),
|
||||
context.getString(R.string.paid_relays_are_not_supported),
|
||||
)
|
||||
textFieldRelay.value = TextFieldValue("")
|
||||
isLoading.value = false
|
||||
return@secondLaunch
|
||||
}
|
||||
|
||||
if (info.limitation?.auth_required == true) {
|
||||
accountStateViewModel.toast(
|
||||
context.getString(R.string.relay),
|
||||
context.getString(R.string.auth_required_message),
|
||||
)
|
||||
textFieldRelay.value = TextFieldValue("")
|
||||
isLoading.value = false
|
||||
return@secondLaunch
|
||||
}
|
||||
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
val encryptedContent = signer.signerSync.nip04Encrypt(
|
||||
"Test bunker event",
|
||||
signer.keyPair.pubKey.toHexKey(),
|
||||
)
|
||||
encryptedContent?.let {
|
||||
val event = signer.signerSync.sign<Event>(
|
||||
TimeUtils.now(),
|
||||
24133,
|
||||
arrayOf(arrayOf("p", signer.keyPair.pubKey.toHexKey())),
|
||||
it,
|
||||
)
|
||||
|
||||
event?.let { signedEvent ->
|
||||
val relay = Relay(
|
||||
addedWSS,
|
||||
read = true,
|
||||
write = true,
|
||||
activeTypes = setOf(),
|
||||
)
|
||||
RelayPool.addRelay(
|
||||
relay,
|
||||
)
|
||||
relay.connect()
|
||||
delay(2000)
|
||||
val result = Client.sendAndWaitForResponse(
|
||||
signedEvent = signedEvent,
|
||||
relayList = listOf(RelaySetupInfo(addedWSS, read = true, write = true, setOf())),
|
||||
)
|
||||
RelayPool.getRelay(addedWSS)?.disconnect()
|
||||
RelayPool.removeRelay(
|
||||
relay,
|
||||
)
|
||||
|
||||
if (result) {
|
||||
relays2.add(
|
||||
RelaySetupInfo(
|
||||
addedWSS,
|
||||
@@ -359,54 +484,58 @@ fun EditDefaultRelaysDialog(
|
||||
feedTypes = COMMON_FEED_TYPES,
|
||||
),
|
||||
)
|
||||
textFieldRelay = TextFieldValue("")
|
||||
} else {
|
||||
accountStateViewModel.toast(
|
||||
context.getString(R.string.relay),
|
||||
context.getString(R.string.failed_to_send_event),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
label = {
|
||||
Text("Relay")
|
||||
},
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
val url = textFieldRelay.text
|
||||
if (url.isNotBlank() && url != "/") {
|
||||
var addedWSS =
|
||||
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
||||
// TODO: How to identify relays on the local network?
|
||||
val isLocalHost = url.contains("127.0.0.1") || url.contains("localhost")
|
||||
if (url.endsWith(".onion") || url.endsWith(".onion/") || isLocalHost) {
|
||||
"ws://$url"
|
||||
} else {
|
||||
"wss://$url"
|
||||
}
|
||||
} else {
|
||||
url
|
||||
}
|
||||
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
|
||||
if (relays2.any { it.url == addedWSS }) {
|
||||
textFieldRelay = TextFieldValue("")
|
||||
return@IconButton
|
||||
}
|
||||
relays2.add(
|
||||
RelaySetupInfo(
|
||||
addedWSS,
|
||||
read = true,
|
||||
write = true,
|
||||
feedTypes = COMMON_FEED_TYPES,
|
||||
),
|
||||
)
|
||||
textFieldRelay = TextFieldValue("")
|
||||
textFieldRelay.value = TextFieldValue("")
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
null,
|
||||
)
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = { dirtyUrl, errorCode, exceptionMessage ->
|
||||
isLoading.value = false
|
||||
val msg =
|
||||
when (errorCode) {
|
||||
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
|
||||
context.getString(
|
||||
R.string.relay_information_document_error_assemble_url,
|
||||
dirtyUrl,
|
||||
exceptionMessage,
|
||||
)
|
||||
|
||||
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
|
||||
context.getString(
|
||||
R.string.relay_information_document_error_assemble_url,
|
||||
dirtyUrl,
|
||||
exceptionMessage,
|
||||
)
|
||||
|
||||
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
|
||||
context.getString(
|
||||
R.string.relay_information_document_error_assemble_url,
|
||||
dirtyUrl,
|
||||
exceptionMessage,
|
||||
)
|
||||
|
||||
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
|
||||
context.getString(
|
||||
R.string.relay_information_document_error_assemble_url,
|
||||
dirtyUrl,
|
||||
exceptionMessage,
|
||||
)
|
||||
}
|
||||
|
||||
accountStateViewModel.toast(
|
||||
context.getString(R.string.unable_to_download_relay_document),
|
||||
msg,
|
||||
)
|
||||
textFieldRelay.value = TextFieldValue("")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,8 +622,8 @@ fun RelayLogDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActiveRelaysDialog(
|
||||
onClose: () -> Unit,
|
||||
fun ActiveRelaysScreen(
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val relays2 =
|
||||
remember {
|
||||
@@ -520,60 +649,44 @@ fun ActiveRelaysDialog(
|
||||
)
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxHeight(0.9f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxHeight(0.9f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
items(relays2.size) {
|
||||
Row(
|
||||
items(relays2.size) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(6.dp)
|
||||
.clickable {
|
||||
relayLogUrl = relays2[it].url
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Circle,
|
||||
contentDescription = "Active Relay",
|
||||
tint = if (RelayPool.getRelay(relays2[it].url)?.isConnected() == true) Color.Green else Color.Red,
|
||||
)
|
||||
Text(
|
||||
relays2[it].url,
|
||||
Modifier
|
||||
.padding(6.dp)
|
||||
.clickable {
|
||||
relayLogUrl = relays2[it].url
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Circle,
|
||||
contentDescription = "Active Relay",
|
||||
tint = if (RelayPool.getRelay(relays2[it].url)?.isConnected() == true) Color.Green else Color.Red,
|
||||
)
|
||||
Text(
|
||||
relays2[it].url,
|
||||
Modifier
|
||||
.weight(0.9f)
|
||||
.padding(8.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
.weight(0.9f)
|
||||
.padding(8.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavController
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BackButtonScaffold(
|
||||
navController: NavController,
|
||||
content: @Composable (paddingValues: PaddingValues) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("")
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
navController.navigateUp()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
content(padding)
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,7 @@ fun EncryptDecryptData(
|
||||
shouldRunOnAccept,
|
||||
remember.value,
|
||||
packageName,
|
||||
false,
|
||||
onAccept,
|
||||
onReject,
|
||||
) {
|
||||
|
||||
@@ -103,6 +103,7 @@ fun EventData(
|
||||
shouldAcceptOrReject,
|
||||
remember.value,
|
||||
packageName,
|
||||
false,
|
||||
onAccept,
|
||||
onReject,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Hub
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.database.AppDatabase
|
||||
import com.greenart7c3.nostrsigner.database.ApplicationEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
|
||||
import com.greenart7c3.nostrsigner.ui.PermissionsFloatingActionButton
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScaffold(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
account: Account,
|
||||
database: AppDatabase,
|
||||
navController: NavController,
|
||||
destinationRoute: String,
|
||||
shouldShowFloatingButton: Boolean,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.Settings)
|
||||
var shouldShowBottomSheet by remember { mutableStateOf(false) }
|
||||
val sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
confirmValueChange = { it != SheetValue.PartiallyExpanded },
|
||||
skipPartiallyExpanded = true,
|
||||
)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline" && shouldShowFloatingButton) {
|
||||
PermissionsFloatingActionButton(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
) {
|
||||
val secret = UUID.randomUUID().toString().substring(0, 6)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val application =
|
||||
ApplicationEntity(
|
||||
secret,
|
||||
"",
|
||||
NostrSigner.getInstance().settings.defaultRelays,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
false,
|
||||
secret,
|
||||
false,
|
||||
account.signPolicy,
|
||||
)
|
||||
|
||||
database.applicationDao().insertApplication(
|
||||
application,
|
||||
)
|
||||
val relayString = NostrSigner.getInstance().settings.defaultRelays.joinToString(separator = "&") { "relay=${it.url}" }
|
||||
val bunkerUrl = "bunker://${account.keyPair.pubKey.toHexKey()}?$relayString"
|
||||
clipboardManager.setText(AnnotatedString(bunkerUrl))
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigate("Permission/$secret")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
if (shouldShowBottomSheet) {
|
||||
AccountsBottomSheet(
|
||||
sheetState = sheetState,
|
||||
account = account,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
onClose = {
|
||||
scope.launch {
|
||||
shouldShowBottomSheet = false
|
||||
sheetState.hide()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
actions = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(40.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
navController.navigate(Route.ActiveRelays.route)
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Hub,
|
||||
contentDescription = "Relays",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
val name = LocalPreferences.getAccountName(context, account.keyPair.pubKey.toNpub())
|
||||
Row(
|
||||
Modifier
|
||||
.border(
|
||||
border = ButtonDefaults.outlinedButtonBorder,
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
.padding(8.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
shouldShowBottomSheet = true
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(name.ifBlank { account.keyPair.pubKey.toNpub().toShortenHex() })
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar(tonalElevation = 0.dp) {
|
||||
items.forEach {
|
||||
val selected = destinationRoute == it.route
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(it.route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
if (selected) it.selectedIcon else it.icon,
|
||||
it.route,
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(it.title)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
content(padding)
|
||||
}
|
||||
}
|
||||
+56
-54
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
@@ -108,6 +109,16 @@ fun MultiEventHomeScreen(
|
||||
val context = LocalContext.current
|
||||
val grouped = intents.groupBy { it.type }.filter { it.key != SignerType.SIGN_EVENT }
|
||||
val grouped2 = intents.filter { it.type == SignerType.SIGN_EVENT }.groupBy { it.event?.kind }
|
||||
val rememberMyChoices = grouped.map {
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
}
|
||||
val rememberMyChoices2 = grouped2.map {
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
@@ -126,14 +137,16 @@ fun MultiEventHomeScreen(
|
||||
LazyColumn(
|
||||
Modifier.fillMaxHeight(0.9f),
|
||||
) {
|
||||
grouped.toList().forEach {
|
||||
grouped.toList().forEachIndexed { index, it ->
|
||||
item {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
Card(
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.clickable {
|
||||
isExpanded = !isExpanded
|
||||
rememberMyChoices[index].value = !rememberMyChoices[index].value
|
||||
it.second.forEach { item ->
|
||||
item.rememberMyChoice.value = rememberMyChoices[index].value
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
@@ -142,17 +155,6 @@ fun MultiEventHomeScreen(
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.run {
|
||||
if (isExpanded) {
|
||||
KeyboardArrowDown
|
||||
} else {
|
||||
KeyboardArrowUp
|
||||
}
|
||||
},
|
||||
contentDescription = "",
|
||||
tint = Color.LightGray,
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -177,24 +179,38 @@ fun MultiEventHomeScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isExpanded) {
|
||||
Column {
|
||||
it.second.forEach {
|
||||
ListItem(it, packageName, intents)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
) {
|
||||
RememberMyChoice(
|
||||
shouldRunAcceptOrReject = null,
|
||||
rememberMyChoices[index].value,
|
||||
null,
|
||||
true,
|
||||
{ },
|
||||
{ },
|
||||
) {
|
||||
rememberMyChoices[index].value = !rememberMyChoices[index].value
|
||||
it.second.forEach { item ->
|
||||
item.rememberMyChoice.value = rememberMyChoices[index].value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grouped2.toList().forEach {
|
||||
grouped2.toList().forEachIndexed { index, it ->
|
||||
item {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
Card(
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.clickable {
|
||||
isExpanded = !isExpanded
|
||||
rememberMyChoices2[index].value = !rememberMyChoices2[index].value
|
||||
it.second.forEach { item ->
|
||||
item.rememberMyChoice.value = rememberMyChoices2[index].value
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
@@ -203,17 +219,6 @@ fun MultiEventHomeScreen(
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.run {
|
||||
if (isExpanded) {
|
||||
KeyboardArrowDown
|
||||
} else {
|
||||
KeyboardArrowUp
|
||||
}
|
||||
},
|
||||
contentDescription = "",
|
||||
tint = Color.LightGray,
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -232,11 +237,23 @@ fun MultiEventHomeScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isExpanded) {
|
||||
Column {
|
||||
it.second.forEach {
|
||||
ListItem(it, packageName, intents)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
) {
|
||||
RememberMyChoice(
|
||||
shouldRunAcceptOrReject = null,
|
||||
rememberMyChoices2[index].value,
|
||||
null,
|
||||
true,
|
||||
{ },
|
||||
{ },
|
||||
) {
|
||||
rememberMyChoices2[index].value = !rememberMyChoices2[index].value
|
||||
it.second.forEach { item ->
|
||||
item.rememberMyChoice.value = rememberMyChoices2[index].value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,7 +554,6 @@ private suspend fun reconnectToRelays(intents: List<IntentData>) {
|
||||
fun ListItem(
|
||||
intentData: IntentData,
|
||||
packageName: String?,
|
||||
intents: List<IntentData>,
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
@@ -639,27 +655,13 @@ fun ListItem(
|
||||
}
|
||||
|
||||
Text(
|
||||
content,
|
||||
content.take(100),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
)
|
||||
RememberMyChoice(
|
||||
shouldRunAcceptOrReject = null,
|
||||
intentData.rememberMyChoice.value,
|
||||
appName,
|
||||
{ },
|
||||
{ },
|
||||
) {
|
||||
intentData.rememberMyChoice.value = !intentData.rememberMyChoice.value
|
||||
intents.filter { item ->
|
||||
intentData.type == item.type
|
||||
}.forEach { item ->
|
||||
item.rememberMyChoice.value = intentData.rememberMyChoice.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ fun RememberMyChoice(
|
||||
shouldRunAcceptOrReject: Boolean?,
|
||||
remember: Boolean,
|
||||
packageName: String?,
|
||||
alwaysShow: Boolean = false,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit,
|
||||
onChanged: () -> Unit,
|
||||
@@ -30,7 +31,7 @@ fun RememberMyChoice(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (packageName != null) {
|
||||
if (packageName != null || alwaysShow) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
@@ -120,6 +120,7 @@ fun SignMessage(
|
||||
shouldRunOnAccept,
|
||||
remember.value,
|
||||
packageName,
|
||||
false,
|
||||
onAccept,
|
||||
onReject,
|
||||
) {
|
||||
|
||||
+71
-34
@@ -131,13 +131,28 @@ fun SingleEventHomeScreen(
|
||||
},
|
||||
{
|
||||
if (intentData.bunkerRequest != null) {
|
||||
AmberUtils.sendBunkerError(
|
||||
account,
|
||||
intentData.bunkerRequest,
|
||||
applicationEntity?.application?.relays ?: emptyList(),
|
||||
context,
|
||||
onLoading = onLoading,
|
||||
)
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
|
||||
val savedApplication = database.applicationDao().getByKey(key)
|
||||
val relays = if (savedApplication != null) {
|
||||
savedApplication.application.relays.ifEmpty { defaultRelays }
|
||||
} else {
|
||||
intentData.bunkerRequest.relays.ifEmpty { defaultRelays }
|
||||
}
|
||||
|
||||
NostrSigner.getInstance().checkForNewRelays(
|
||||
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
|
||||
relays.toSet(),
|
||||
)
|
||||
|
||||
AmberUtils.sendBunkerError(
|
||||
account,
|
||||
intentData.bunkerRequest,
|
||||
applicationEntity?.application?.relays ?: intentData.bunkerRequest.relays,
|
||||
context,
|
||||
onLoading = onLoading,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
context.getAppCompatActivity()?.intent = null
|
||||
context.getAppCompatActivity()?.finish()
|
||||
@@ -208,8 +223,19 @@ fun SingleEventHomeScreen(
|
||||
)
|
||||
}
|
||||
|
||||
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
|
||||
val savedApplication = database.applicationDao().getByKey(key)
|
||||
val relays = intentData.bunkerRequest?.relays ?: listOf()
|
||||
val relays = if (savedApplication != null) {
|
||||
savedApplication.application.relays.ifEmpty { defaultRelays }
|
||||
} else {
|
||||
intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays
|
||||
}
|
||||
if (intentData.bunkerRequest != null) {
|
||||
NostrSigner.getInstance().checkForNewRelays(
|
||||
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
|
||||
relays.toSet(),
|
||||
)
|
||||
}
|
||||
val application =
|
||||
savedApplication ?: ApplicationWithPermissions(
|
||||
application = ApplicationEntity(
|
||||
@@ -240,19 +266,19 @@ fun SingleEventHomeScreen(
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (intentData.bunkerRequest != null) {
|
||||
AmberUtils.sendBunkerError(
|
||||
account,
|
||||
intentData.bunkerRequest,
|
||||
applicationEntity?.application?.relays ?: emptyList(),
|
||||
context,
|
||||
onLoading,
|
||||
)
|
||||
} else {
|
||||
context.getAppCompatActivity()?.intent = null
|
||||
context.getAppCompatActivity()?.finish()
|
||||
if (intentData.bunkerRequest != null) {
|
||||
AmberUtils.sendBunkerError(
|
||||
account,
|
||||
intentData.bunkerRequest,
|
||||
relays,
|
||||
context,
|
||||
onLoading,
|
||||
)
|
||||
} else {
|
||||
context.getAppCompatActivity()?.intent = null
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -337,9 +363,20 @@ fun SingleEventHomeScreen(
|
||||
database,
|
||||
)
|
||||
}
|
||||
|
||||
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
|
||||
val savedApplication = database.applicationDao().getByKey(key)
|
||||
val relays = intentData.bunkerRequest?.relays ?: listOf()
|
||||
val relays = if (savedApplication != null) {
|
||||
savedApplication.application.relays.ifEmpty { defaultRelays }
|
||||
} else {
|
||||
intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays
|
||||
}
|
||||
if (intentData.bunkerRequest != null) {
|
||||
NostrSigner.getInstance().checkForNewRelays(
|
||||
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
|
||||
relays.toSet(),
|
||||
)
|
||||
}
|
||||
|
||||
val application =
|
||||
savedApplication ?: ApplicationWithPermissions(
|
||||
application = ApplicationEntity(
|
||||
@@ -370,19 +407,19 @@ fun SingleEventHomeScreen(
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (intentData.bunkerRequest != null) {
|
||||
AmberUtils.sendBunkerError(
|
||||
account,
|
||||
intentData.bunkerRequest,
|
||||
applicationEntity?.application?.relays ?: emptyList(),
|
||||
context,
|
||||
onLoading,
|
||||
)
|
||||
} else {
|
||||
context.getAppCompatActivity()?.intent = null
|
||||
context.getAppCompatActivity()?.finish()
|
||||
if (intentData.bunkerRequest != null) {
|
||||
AmberUtils.sendBunkerError(
|
||||
account,
|
||||
intentData.bunkerRequest,
|
||||
relays,
|
||||
context,
|
||||
onLoading,
|
||||
)
|
||||
} else {
|
||||
context.getAppCompatActivity()?.intent = null
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -46,4 +46,60 @@ sealed class Route(
|
||||
icon = Icons.AutoMirrored.Outlined.ViewList,
|
||||
selectedIcon = Icons.AutoMirrored.Default.ViewList,
|
||||
)
|
||||
|
||||
data object AccountBackup : Route(
|
||||
title = "",
|
||||
route = "AccountBackup",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object Logs : Route(
|
||||
title = "",
|
||||
route = "Logs",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object ActiveRelays : Route(
|
||||
title = "",
|
||||
route = "ActiveRelays",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object Language : Route(
|
||||
title = "",
|
||||
route = "Language",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object NotificationType : Route(
|
||||
title = "",
|
||||
route = "NotificationType",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object DefaultRelays : Route(
|
||||
title = "",
|
||||
route = "DefaultRelays",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object SignPolicy : Route(
|
||||
title = "",
|
||||
route = "SignPolicy",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
|
||||
data object Security : Route(
|
||||
title = "",
|
||||
route = "Security",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -328,24 +328,50 @@
|
||||
<string name="nip_not_found">NIP-%1$s wurde nicht gefunden</string>
|
||||
<string name="nip_not_found_for_the_event_kind">NIP für Event-Art %1$s nicht gefunden</string>
|
||||
<string name="use_secret_to_connect_to_the_application">Geheimnis verwenden, um sich mit der Anwendung zu verbinden</string>
|
||||
<string name="add_a_key">Add a key</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="add_your_key">Add your key</string>
|
||||
<string name="sign_policy_basic">Approve basic actions</string>
|
||||
<string name="sign_policy_basic_explainer">Recommended for most people. This will minimize the number of interruptions during your app usage.</string>
|
||||
<string name="sign_policy_manual">Manually approve each app</string>
|
||||
<string name="sign_policy_manual_explainer">Recommended for privacy-minded people who would like control over each app. Choosing this will prompt you set a preference every time you try a new app.</string>
|
||||
<string name="sign_policy_title">Select a signing policy</string>
|
||||
<string name="sign_policy_explainer">Should I approve nostr events automatically or would you like to review them for each app?</string>
|
||||
<string name="finish">Finish</string>
|
||||
<string name="seed_words_title">Your recovery phrase</string>
|
||||
<string name="seed_words_explainer">These words let you recreate your account in case you lose access. Write them down in a safe place</string>
|
||||
<string name="show_seed_words">Show seed words</string>
|
||||
<string name="sign_policy">Sign policy</string>
|
||||
<string name="handle_application_permissions">How to handle this application permissions?</string>
|
||||
<string name="sign_policy_manual_new_app">Manually approve each permission</string>
|
||||
<string name="add_a_key">Schlüssel hinzufügen</string>
|
||||
<string name="next">Weiter</string>
|
||||
<string name="add_your_key">Deinen Schlüssel hinzufügen</string>
|
||||
<string name="sign_policy_basic">Basis-Aktionen genehmigen</string>
|
||||
<string name="sign_policy_basic_explainer">Empfohlen für die meisten Leute. Dadurch wird die Anzahl der Unterbrechungen während der Nutzung deiner App minimiert.</string>
|
||||
<string name="sign_policy_manual">Jede App manuell genehmigen</string>
|
||||
<string name="sign_policy_manual_explainer">Empfohlen für auf Privatsphäre bedachte Personen, die die Kontrolle über jede App haben möchten. Wenn du diese Option wählst, wirst du jedes Mal, wenn du eine neue App ausprobierst, aufgefordert, eine Einstellung festzulegen.</string>
|
||||
<string name="sign_policy_title">Wähle eine Signierrichtlinie</string>
|
||||
<string name="sign_policy_explainer">Soll ich Nostr-Events automatisch genehmigen oder möchtest du sie für jede App überprüfen?</string>
|
||||
<string name="finish">Abschließen</string>
|
||||
<string name="seed_words_title">Deine Wiederherstellungsphrase</string>
|
||||
<string name="seed_words_explainer">Mit diesen Wörtern kannst du dein Konto wiederherstellen, falls du den Zugriff verlierst. Schreibe sie an einem sicheren Ort auf</string>
|
||||
<string name="show_seed_words">Seed-Wörter anzeigen</string>
|
||||
<string name="sign_policy">Signierrichtlinie</string>
|
||||
<string name="handle_application_permissions">Wie soll ich mit den Berechtigungen dieser Anwendung umgehen?</string>
|
||||
<string name="sign_policy_manual_new_app">Jede Berechtigung manuell genehmigen</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recommended for privacy-minded people who would like control over each permission. Choosing this will prompt you set a preference every time you receive a new permission.</string>
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="sign_policy_fully">Ich vertraue dieser Anwendung voll und ganz</string>
|
||||
<string name="sign_policy_fully_explainer">Jede Anfrage automatisch signieren</string>
|
||||
<string name="event_kind_10050">DN-Relais</string>
|
||||
<string name="paste_from_clipboard">Aus Zwischenablage einfügen</string>
|
||||
<string name="invalid_nostr_connect_uri">Ungültige Nostr-Connect-URL</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -30,52 +30,52 @@
|
||||
<string name="no_permissions_granted">No se han concedido permisos</string>
|
||||
<string name="remember_my_choice_and_don_t_ask_again">Recordar mi elección y no volver a preguntar</string>
|
||||
<string name="reject">Rechazar</string>
|
||||
<string name="accept">Accept</string>
|
||||
<string name="encrypt_nip44">encrypt nip44</string>
|
||||
<string name="encrypt_nip04">encrypt nip04</string>
|
||||
<string name="decrypt_nip44">decrypt nip44</string>
|
||||
<string name="decrypt_nip04">decrypt nip04</string>
|
||||
<string name="decrypt_zap_event">decrypt zap event</string>
|
||||
<string name="encrypt_decrypt">encrypt/decrypt</string>
|
||||
<string name="show_details">Show Details</string>
|
||||
<string name="hide_details">Hide Details</string>
|
||||
<string name="copy_raw_json">Raw json</string>
|
||||
<string name="add_new_account">Add New Account</string>
|
||||
<string name="select_account">Select Account</string>
|
||||
<string name="active_account">Active Account</string>
|
||||
<string name="edit_name">Edit name</string>
|
||||
<string name="copy">"Copy "</string>
|
||||
<string name="encrypted_decrypted_data">Encrypted/Decrypted data</string>
|
||||
<string name="data_copied_to_the_clipboard">Data copied to the clipboard</string>
|
||||
<string name="connect_app">Connect app</string>
|
||||
<string name="event_copied_to_the_clipboard">Event copied to the clipboard</string>
|
||||
<string name="select_deselect_all">Select/Deselect all</string>
|
||||
<string name="decrypt_content">Decrypt content</string>
|
||||
<string name="support_development">Support development</string>
|
||||
<string name="source_code">Source code</string>
|
||||
<string name="show_qr_code">Show QR Code</string>
|
||||
<string name="shows_qr_code_with_you_private_key">Shows QR Code with you private key</string>
|
||||
<string name="accept">Aceptar</string>
|
||||
<string name="encrypt_nip44">encriptar nip44</string>
|
||||
<string name="encrypt_nip04">encriptar nip04</string>
|
||||
<string name="decrypt_nip44">desencriptar nip44</string>
|
||||
<string name="decrypt_nip04">desencriptar nip04</string>
|
||||
<string name="decrypt_zap_event">desencriptar evento de zapeo</string>
|
||||
<string name="encrypt_decrypt">encriptar/desencriptar</string>
|
||||
<string name="show_details">Mostrar detalles</string>
|
||||
<string name="hide_details">Ocultar detalles</string>
|
||||
<string name="copy_raw_json">Json crudo</string>
|
||||
<string name="add_new_account">Agregar nueva cuenta</string>
|
||||
<string name="select_account">Seleccionar Cuenta</string>
|
||||
<string name="active_account">Activar cuenta</string>
|
||||
<string name="edit_name">Editar nombre</string>
|
||||
<string name="copy">"Copiar "</string>
|
||||
<string name="encrypted_decrypted_data">Datos cifrados/descifrados</string>
|
||||
<string name="data_copied_to_the_clipboard"></string>
|
||||
<string name="connect_app">Conectar app</string>
|
||||
<string name="event_copied_to_the_clipboard">Evento copiado al portapapeles</string>
|
||||
<string name="select_deselect_all">Seleccionar/Deseleccionar todo</string>
|
||||
<string name="decrypt_content">Desencriptar contenido</string>
|
||||
<string name="support_development">Apoya el desarrollo</string>
|
||||
<string name="source_code">Código fuente</string>
|
||||
<string name="show_qr_code">Mostrar código QR</string>
|
||||
<string name="shows_qr_code_with_you_private_key">Muestra código QR con tu clave privada</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password.
|
||||
\n\nIf you lose your password, you will not be able to recover your key.
|
||||
</string>
|
||||
<string name="ncryptsec_password">password to open the key</string>
|
||||
<string name="password_is_required">Password is required</string>
|
||||
<string name="failed_to_encrypt_key">Failed to encrypt your private key</string>
|
||||
<string name="biometric_authentication_failed_explainer">Biometrics failed to authenticate the owner of this phone</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">Biometrics failed to authenticate the owner of this phone. Error: %1$s</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Encrypt and copy my secret key</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="permissions">Permissions</string>
|
||||
<string name="delete_application">Delete Application</string>
|
||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||
<string name="nothing_to_approve_yet">Nothing to approve yet</string>
|
||||
<string name="why_not_explore_your_favorite_nostr_app_a_bit">Why not explore your favorite nostr app a bit?</string>
|
||||
<string name="grant_permissions">Grant Permissions</string>
|
||||
<string name="adjust">Adjust</string>
|
||||
<string name="more">%1$s more...</string>
|
||||
<string name="and_more">...and more</string>
|
||||
<string name="would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf">" would like your permission to read your public key and sign events on your behalf"</string>
|
||||
<string name="new_app">New App</string>
|
||||
<string name="ncryptsec_password">contraseña para abrir la llave</string>
|
||||
<string name="password_is_required">La contraseña es obligatoria</string>
|
||||
<string name="failed_to_encrypt_key">No se pudo cifrar su clave privada</string>
|
||||
<string name="biometric_authentication_failed_explainer">La biometría no ha podido autenticar al propietario de este teléfono</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">La biometría no ha podido autenticar al propietario de este teléfono</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Encriptar y copiar mi clave secreta</string>
|
||||
<string name="delete">Borrar</string>
|
||||
<string name="permissions">Permisos</string>
|
||||
<string name="delete_application">Eliminar aplicación</string>
|
||||
<string name="copy_to_clipboard">Copiar al portapapeles</string>
|
||||
<string name="nothing_to_approve_yet">Nada que aprobar aún</string>
|
||||
<string name="why_not_explore_your_favorite_nostr_app_a_bit">¿Por qué no explorar un poco tu app favorita de nostr?</string>
|
||||
<string name="grant_permissions">Otorgar Permisos</string>
|
||||
<string name="adjust">Ajustar</string>
|
||||
<string name="more">%1$s más...</string>
|
||||
<string name="and_more">... y más</string>
|
||||
<string name="would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf">" desea tu permiso para leer tu clave pública y firmar eventos en tu nombre"</string>
|
||||
<string name="new_app">Nueva App</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Connect through Tor with Orbot
|
||||
\n\n1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Start Orbot
|
||||
@@ -84,13 +84,13 @@
|
||||
\n5. Configure the Socks port in this screen
|
||||
\n6. Press the Activate button to use Orbot as a proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||
<string name="invalid_port_number">Invalid port number</string>
|
||||
<string name="use_orbot">Use Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Disconnect Tor/Orbot</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot setup</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Disconnect from your Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Your data will be immediately transferred in the regular network</string>
|
||||
<string name="orbot_socks_port">Puerto Orbot Socks</string>
|
||||
<string name="invalid_port_number">Número de puerto no válido</string>
|
||||
<string name="use_orbot">Usar Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Desconectar Tor/Orbot</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Desconectar de Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Sus datos serán transferidos inmediatamente en la red regular</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="could_not_connect_to_tor">Could not connect to Tor</string>
|
||||
@@ -348,4 +348,13 @@
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||
<string name="invalid_nostr_connect_uri">Invalid nostr connect URI</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
</resources>
|
||||
|
||||
@@ -1 +1,377 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"><string name="app_name_debug" translatable="false">Amber Debug</string></resources>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Key Backup and Safety Tips
|
||||
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
|
||||
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
||||
\n- Amber developers will **never** ask you for your secret key.
|
||||
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Clave secreta (nsec) copiada al portapapeles</string>
|
||||
<string name="copy_my_secret_key">Copiar mi clave secreta</string>
|
||||
<string name="biometric_authentication_failed">Autenticación fallida</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copia el ID Nsec(tu contraseña) al portapapeles para hacer una copia de seguridad</string>
|
||||
<string name="event_pubkey_is_not_equal_to_current_logged_in_user">Clave pública del evento no coincide con el usuario logueado</string>
|
||||
<string name="signature_copied_to_the_clipboard">Firma copiada al portapapeles</string>
|
||||
<string name="reset_permissions">Restablecer permisos</string>
|
||||
<string name="logout">Cerrar sesión</string>
|
||||
<string name="logging_out_deletes_all_your_local_information_make_sure_to_have_your_private_keys_backed_up_to_avoid_losing_your_account_do_you_want_to_continue">Cerrar la sesión borra toda su información local. Asegúrate de tener tus claves privadas respaldadas para evitar perder tu cuenta. ¿Quieres continuar?</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="do_you_want_to_reset_all_permissions">¿Quieres restablecer todos los permisos?</string>
|
||||
<string name="confirm">Confirmar</string>
|
||||
<string name="nsec">Nsec / clave privada</string>
|
||||
<string name="show_password">Mostrar contraseña</string>
|
||||
<string name="hide_password">Ocultar contraseña</string>
|
||||
<string name="invalid_key">Clave inválida</string>
|
||||
<string name="key_is_required">Se requiere una llave</string>
|
||||
<string name="login">Iniciar sesión</string>
|
||||
<string name="generate_a_new_key">Generar una nueva clave</string>
|
||||
<string name="backup_keys">Respaldar claves</string>
|
||||
<string name="read_your_public_key">Leer su clave pública</string>
|
||||
<string name="no_permissions_granted">No se han concedido permisos</string>
|
||||
<string name="remember_my_choice_and_don_t_ask_again">Recordar mi elección y no volver a preguntar</string>
|
||||
<string name="reject">Rechazar</string>
|
||||
<string name="accept">Aceptar</string>
|
||||
<string name="encrypt_nip44">encriptar nip44</string>
|
||||
<string name="encrypt_nip04">encriptar nip04</string>
|
||||
<string name="decrypt_nip44">desencriptar nip44</string>
|
||||
<string name="decrypt_nip04">desencriptar nip04</string>
|
||||
<string name="decrypt_zap_event">desencriptar evento de zapeo</string>
|
||||
<string name="encrypt_decrypt">encriptar/desencriptar</string>
|
||||
<string name="show_details">Mostrar detalles</string>
|
||||
<string name="hide_details">Ocultar detalles</string>
|
||||
<string name="copy_raw_json">Json crudo</string>
|
||||
<string name="add_new_account">Agregar nueva cuenta</string>
|
||||
<string name="select_account">Seleccionar Cuenta</string>
|
||||
<string name="active_account">Activar cuenta</string>
|
||||
<string name="edit_name">Editar nombre</string>
|
||||
<string name="copy">"Copiar "</string>
|
||||
<string name="encrypted_decrypted_data">Datos cifrados/descifrados</string>
|
||||
<string name="data_copied_to_the_clipboard"></string>
|
||||
<string name="connect_app">Conectar app</string>
|
||||
<string name="event_copied_to_the_clipboard">Evento copiado al portapapeles</string>
|
||||
<string name="select_deselect_all">Seleccionar/Deseleccionar todo</string>
|
||||
<string name="decrypt_content">Desencriptar contenido</string>
|
||||
<string name="support_development">Apoya el desarrollo</string>
|
||||
<string name="source_code">Código fuente</string>
|
||||
<string name="show_qr_code">Mostrar código QR</string>
|
||||
<string name="shows_qr_code_with_you_private_key">Muestra código QR con tu clave privada</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password.
|
||||
\n\nIf you lose your password, you will not be able to recover your key.
|
||||
</string>
|
||||
<string name="ncryptsec_password">contraseña para abrir la llave</string>
|
||||
<string name="password_is_required">La contraseña es obligatoria</string>
|
||||
<string name="failed_to_encrypt_key">No se pudo cifrar su clave privada</string>
|
||||
<string name="biometric_authentication_failed_explainer">La biometría no ha podido autenticar al propietario de este teléfono</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">La biometría no ha podido autenticar al propietario de este teléfono</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Encriptar y copiar mi clave secreta</string>
|
||||
<string name="delete">Borrar</string>
|
||||
<string name="permissions">Permisos</string>
|
||||
<string name="delete_application">Eliminar aplicación</string>
|
||||
<string name="copy_to_clipboard">Copiar al portapapeles</string>
|
||||
<string name="nothing_to_approve_yet">Nada que aprobar aún</string>
|
||||
<string name="why_not_explore_your_favorite_nostr_app_a_bit">¿Por qué no explorar un poco tu app favorita de nostr?</string>
|
||||
<string name="grant_permissions">Otorgar Permisos</string>
|
||||
<string name="adjust">Ajustar</string>
|
||||
<string name="more">%1$s más...</string>
|
||||
<string name="and_more">... y más</string>
|
||||
<string name="would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf">" desea tu permiso para leer tu clave pública y firmar eventos en tu nombre"</string>
|
||||
<string name="new_app">Nueva App</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Connect through Tor with Orbot
|
||||
\n\n1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Start Orbot
|
||||
\n3. In Orbot, check the Socks port. The default uses 9050
|
||||
\n4. If necessary change the port in Orbot
|
||||
\n5. Configure the Socks port in this screen
|
||||
\n6. Press the Activate button to use Orbot as a proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Puerto Orbot Socks</string>
|
||||
<string name="invalid_port_number">Número de puerto no válido</string>
|
||||
<string name="use_orbot">Usar Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Desconectar Tor/Orbot</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Desconectar de Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Sus datos serán transferidos inmediatamente en la red regular</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="could_not_connect_to_tor">Could not connect to Tor</string>
|
||||
<string name="relays">Relays</string>
|
||||
<string name="push_notifications">Push notifications</string>
|
||||
<string name="direct_connection">Direct Connection</string>
|
||||
<string name="notification_type">Notification Type</string>
|
||||
<string name="select_the_type_of_notification_you_want_to_receive">Select the type of notification you want to receive</string>
|
||||
<string name="activity">Activity</string>
|
||||
<string name="accepted">Accepted</string>
|
||||
<string name="rejected">Rejected</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="language_description">For the App\'s Interface</string>
|
||||
<string name="no_activities_found">No activities found</string>
|
||||
<string name="home">Home</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="encrypt_data_using_nip_4">Encrypt data using nip 4</string>
|
||||
<string name="decrypt_data_using_nip_4">Decrypt data using nip 4</string>
|
||||
<string name="decrypt_data_using_nip_44">Decrypt data using nip 44</string>
|
||||
<string name="encrypt_data_using_nip_44">Encrypt data using nip 44</string>
|
||||
<string name="decrypt_private_zaps">Decrypt private zaps</string>
|
||||
<string name="event_kind_0">Metadata</string>
|
||||
<string name="event_kind_1">Short text note</string>
|
||||
<string name="event_kind_3">Follows</string>
|
||||
<string name="event_kind_4">Encrypted direct messages</string>
|
||||
<string name="event_kind_5">Event deletion</string>
|
||||
<string name="event_kind_6">Repost</string>
|
||||
<string name="event_kind_7">Reaction</string>
|
||||
<string name="event_kind_8">Badge award</string>
|
||||
<string name="event_kind_9">Group chat message</string>
|
||||
<string name="event_kind_10">Group chat threaded reply</string>
|
||||
<string name="event_kind_11">Group thread</string>
|
||||
<string name="event_kind_12">Group thread reply</string>
|
||||
<string name="event_kind_13">Seal</string>
|
||||
<string name="event_kind_16">Generic repost</string>
|
||||
<string name="event_kind_40">Channel creation</string>
|
||||
<string name="event_kind_41">Channel metadata</string>
|
||||
<string name="event_kind_42">Channel message</string>
|
||||
<string name="event_kind_43">Channel hide message</string>
|
||||
<string name="event_kind_44">Channel mute user</string>
|
||||
<string name="event_kind_1021">Bid</string>
|
||||
<string name="event_kind_1022">Bid confirmation</string>
|
||||
<string name="event_kind_1040">OpenTimestamps</string>
|
||||
<string name="event_kind_1059">Gift wrap</string>
|
||||
<string name="event_kind_1063">File metadata</string>
|
||||
<string name="event_kind_1311">Live chat message</string>
|
||||
<string name="event_kind_1971">Problem tracker</string>
|
||||
<string name="event_kind_1984">Reporting</string>
|
||||
<string name="event_kind_1985">Label</string>
|
||||
<string name="event_kind_4550">Community post approval</string>
|
||||
<string name="event_kind_5000_5999">Job request</string>
|
||||
<string name="event_kind_6000_6999">Job result</string>
|
||||
<string name="event_kind_7000">Job feedback</string>
|
||||
<string name="event_kind_9000_9030">Group control events</string>
|
||||
<string name="event_kind_9041">Zap goal</string>
|
||||
<string name="event_kind">Event kind %1$s</string>
|
||||
<string name="event_kind_34550">Community definition</string>
|
||||
<string name="event_kind_39000_39009">Group metadata events</string>
|
||||
<string name="event_kind_9734">Zap request</string>
|
||||
<string name="event_kind_9735">Zap</string>
|
||||
<string name="event_kind_9802">Highlights</string>
|
||||
<string name="event_kind_10000">Mute list</string>
|
||||
<string name="event_kind_10001">Pin list</string>
|
||||
<string name="event_kind_10002">Relay list metadata</string>
|
||||
<string name="event_kind_10003">Bookmark list</string>
|
||||
<string name="event_kind_10004">Communities list</string>
|
||||
<string name="event_kind_10005">Public chats list</string>
|
||||
<string name="event_kind_10006">Blocked relays list</string>
|
||||
<string name="event_kind_10007">Search relays list</string>
|
||||
<string name="event_kind_10009">User groups</string>
|
||||
<string name="event_kind_10015">Interests list</string>
|
||||
<string name="event_kind_10030">User emoji list</string>
|
||||
<string name="event_kind_10096">File storage server list</string>
|
||||
<string name="event_kind_13194">Wallet Info</string>
|
||||
<string name="event_kind_21000">Lightning Pub RPC</string>
|
||||
<string name="event_kind_22242">Client authentication</string>
|
||||
<string name="event_kind_23194">Wallet request</string>
|
||||
<string name="event_kind_23195">Wallet response</string>
|
||||
<string name="event_kind_24133">Nostr connect</string>
|
||||
<string name="event_kind_27235">HTTP auth</string>
|
||||
<string name="event_kind_30000">Follow sets</string>
|
||||
<string name="event_kind_30001">Generic lists</string>
|
||||
<string name="event_kind_30002">Relay sets</string>
|
||||
<string name="event_kind_30003">Bookmark sets</string>
|
||||
<string name="event_kind_30004">Curation sets</string>
|
||||
<string name="event_kind_30008">Profile badges</string>
|
||||
<string name="event_kind_30009">Badge definition</string>
|
||||
<string name="event_kind_30015">Interest sets</string>
|
||||
<string name="event_kind_30017">Create or update a stall</string>
|
||||
<string name="event_kind_30018">Create or update a product</string>
|
||||
<string name="event_kind_30019">Marketplace UI/UX</string>
|
||||
<string name="event_kind_30020">Product sold as an auction</string>
|
||||
<string name="event_kind_30023">Long-form content</string>
|
||||
<string name="event_kind_30024">Draft Long-form content</string>
|
||||
<string name="event_kind_30030">Emoji sets</string>
|
||||
<string name="event_kind_30063">Release artifact sets</string>
|
||||
<string name="event_kind_30078">Application-specific data</string>
|
||||
<string name="event_kind_30311">Live event</string>
|
||||
<string name="event_kind_30315">User statuses</string>
|
||||
<string name="event_kind_30402">Classified listing</string>
|
||||
<string name="event_kind_30403">Draft classified listing</string>
|
||||
<string name="event_kind_31234">Generic draft event</string>
|
||||
<string name="event_kind_31922">Date-Based calendar event</string>
|
||||
<string name="event_kind_31923">Time-Based calendar event</string>
|
||||
<string name="event_kind_31924">Calendar</string>
|
||||
<string name="event_kind_31925">Calendar event RSVP</string>
|
||||
<string name="event_kind_31989">Handler recommendation</string>
|
||||
<string name="event_kind_31990">Handler information</string>
|
||||
<string name="connect">Connect</string>
|
||||
<string name="wants_you_to_sign_a">wants you to sign a %1$s</string>
|
||||
<string name="wants_you_to">wants you to %1$s</string>
|
||||
<string name="sign">Sign %1$s</string>
|
||||
<string name="sign_message">sign message</string>
|
||||
<string name="content">content</string>
|
||||
<string name="checking_connectivity">Checking Connectivity</string>
|
||||
<string name="amber_is_running_in_background">Amber is running in background</string>
|
||||
<string name="bunker_notifications">Bunker Notifications</string>
|
||||
<string name="notifications_for_approving_or_rejecting_bunker_requests">Notifications for Approving or Rejecting Bunker requests.</string>
|
||||
<string name="new_event_to_sign">new event to sign</string>
|
||||
<string name="event_kind_10013">Private outbox relay list</string>
|
||||
<string name="remove_permission">Remove permission</string>
|
||||
<string name="connect_through_your_orbot_setup">Connect through your Orbot setup</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="clear_logs">Clear logs</string>
|
||||
<string name="default_relays">Default relays</string>
|
||||
<string name="distributors">Distributors</string>
|
||||
<string name="select_distributor">Select the push notification distributor</string>
|
||||
<string name="select_push_server">Select a UnifiedPush App</string>
|
||||
<string name="push_server_install_app">Push Notification Setup</string>
|
||||
<string name="push_server_install_app_description"> To receive push notifications, install any app that supports [Unified Push](https://unifiedpush.org/), such as [Nfty](https://ntfy.sh/).
|
||||
After installing, select the app you want to use in the Settings.
|
||||
</string>
|
||||
<string name="push_server_uses_app_explainer">Uses app %1$s</string>
|
||||
<string name="push_server_title">Push Notification</string>
|
||||
<string name="push_server_explainer">From installed UnifiedPush apps</string>
|
||||
<string name="quick_action_dont_show_again_button">Don\'t show again</string>
|
||||
<string name="error_dialog_button_ok">OK</string>
|
||||
<string name="push_server_none">None</string>
|
||||
<string name="push_server_none_explainer">Disables Push Notifications</string>
|
||||
<string name="push_server">Push Server</string>
|
||||
<string name="no_distributors_found">No distributors found for push notifications.\nPlease install a distributor like ntfy.</string>
|
||||
<string name="requests">requests</string>
|
||||
<string name="disable_listening_for_new_connections">Disable listening for new connections</string>
|
||||
<string name="enable_listening_for_new_connections">Enable listening for new connections</string>
|
||||
<string name="authenticate">Authentication needed to use the app</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="enable_biometrics">Enable biometrics</string>
|
||||
<string name="when_to_ask">When to ask</string>
|
||||
<string name="every_time">Every time</string>
|
||||
<string name="one_minute">One minute</string>
|
||||
<string name="five_minutes">Five minutes</string>
|
||||
<string name="ten_minutes">Ten Minutes</string>
|
||||
<string name="event_kind_9000">Add user to group</string>
|
||||
<string name="event_kind_9001">Remove user from group</string>
|
||||
<string name="event_kind_9002">Edit group metadata</string>
|
||||
<string name="event_kind_9003">Add permission</string>
|
||||
<string name="event_kind_9004">Remove permission</string>
|
||||
<string name="event_kind_9005">Delete event from group</string>
|
||||
<string name="event_kind_9006">Edit group status</string>
|
||||
<string name="event_kind_9007">Create group</string>
|
||||
<string name="event_kind_9021">Join group</string>
|
||||
<string name="event_kind_39000">Group metadata</string>
|
||||
<string name="event_kind_39001">Group admins</string>
|
||||
<string name="event_kind_39002">Group members</string>
|
||||
<string name="more_info">More information about this permission</string>
|
||||
<string name="nip_kind">Nip %1$s</string>
|
||||
<string name="nip_01">Basic protocol flow description</string>
|
||||
<string name="nip_02">Follow List</string>
|
||||
<string name="nip_03">OpenTimestamps Attestations for Events</string>
|
||||
<string name="nip_04">Encrypted Direct Message</string>
|
||||
<string name="nip_05">Mapping Nostr keys to DNS-based internet identifiers</string>
|
||||
<string name="nip_06">Basic key derivation from mnemonic seed phrase</string>
|
||||
<string name="nip_07">window.nostr capability for web browsers</string>
|
||||
<string name="nip_08">Handling Mentions</string>
|
||||
<string name="nip_09">Event Deletion</string>
|
||||
<string name="nip_10">Conventions for clients</string>
|
||||
<string name="nip_11">Relay Information Document</string>
|
||||
<string name="nip_13">Proof of Work</string>
|
||||
<string name="nip_14">Subject tag in text events</string>
|
||||
<string name="nip_15">Nostr Marketplace (for resilient marketplaces)</string>
|
||||
<string name="nip_17">Private Direct Messages</string>
|
||||
<string name="nip_18">Reposts</string>
|
||||
<string name="nip_19">bech32-encoded entities</string>
|
||||
<string name="nip_21">nostr: URI scheme</string>
|
||||
<string name="nip_23">Long-form Content</string>
|
||||
<string name="nip_24">Extra metadata fields and tags</string>
|
||||
<string name="nip_25">Reactions</string>
|
||||
<string name="nip_26">Delegated Event Signing</string>
|
||||
<string name="nip_27">Text Note References</string>
|
||||
<string name="nip_28">Public Chat</string>
|
||||
<string name="nip_29">Relay-based Groups</string>
|
||||
<string name="nip_30">Custom Emoji</string>
|
||||
<string name="nip_31">Dealing with Unknown Events</string>
|
||||
<string name="nip_32">Labeling</string>
|
||||
<string name="nip_34">git stuff</string>
|
||||
<string name="nip_35">Torrents</string>
|
||||
<string name="nip_36">Sensitive Content</string>
|
||||
<string name="nip_38">User Statuses</string>
|
||||
<string name="nip_39">External Identities in Profiles</string>
|
||||
<string name="nip_40">Expiration Timestamp</string>
|
||||
<string name="nip_42">Authentication of clients to relays</string>
|
||||
<string name="nip_44">Versioned Encryption</string>
|
||||
<string name="nip_45">Counting results</string>
|
||||
<string name="nip_46">Nostr Connect</string>
|
||||
<string name="nip_47">Wallet Connect</string>
|
||||
<string name="nip_48">Proxy Tags</string>
|
||||
<string name="nip_49">Private Key Encryption</string>
|
||||
<string name="nip_50">Search Capability</string>
|
||||
<string name="nip_51">Lists</string>
|
||||
<string name="nip_52">Calendar Events</string>
|
||||
<string name="nip_53">Live Activities</string>
|
||||
<string name="nip_54">Wiki</string>
|
||||
<string name="nip_55">Android Signer Application</string>
|
||||
<string name="nip_56">Reporting</string>
|
||||
<string name="nip_57">Lightning Zaps</string>
|
||||
<string name="nip_58">Badges</string>
|
||||
<string name="nip_59">Gift Wrap</string>
|
||||
<string name="nip_65">Relay List Metadata</string>
|
||||
<string name="nip_70">Protected Events</string>
|
||||
<string name="nip_71">Video Events</string>
|
||||
<string name="nip_72">Moderated Communities</string>
|
||||
<string name="nip_75">Zap Goals</string>
|
||||
<string name="nip_78">Application-specific data</string>
|
||||
<string name="nip_84">Highlights</string>
|
||||
<string name="nip_89">Recommended Application Handlers</string>
|
||||
<string name="nip_90">Data Vending Machines</string>
|
||||
<string name="nip_92">Media Attachments</string>
|
||||
<string name="nip_94">File Metadata</string>
|
||||
<string name="nip_96">HTTP File Storage Integration</string>
|
||||
<string name="nip_98">HTTP Auth</string>
|
||||
<string name="nip_99">Classified Listings</string>
|
||||
<string name="no_information_available_for">No information available for %1$s</string>
|
||||
<string name="nip_not_found">Nip %1$s not found</string>
|
||||
<string name="nip_not_found_for_the_event_kind">Nip not found for the event kind %1$s</string>
|
||||
<string name="use_secret_to_connect_to_the_application">Use secret to connect to the application</string>
|
||||
<string name="add_a_key">Add a key</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="add_your_key">Add your key</string>
|
||||
<string name="sign_policy_basic">Approve basic actions</string>
|
||||
<string name="sign_policy_basic_explainer">Recommended for most people. This will minimize the number of interruptions during your app usage.</string>
|
||||
<string name="sign_policy_manual">Manually approve each app</string>
|
||||
<string name="sign_policy_manual_explainer">Recommended for privacy-minded people who would like control over each app. Choosing this will prompt you set a preference every time you try a new app.</string>
|
||||
<string name="sign_policy_title">Select a signing policy</string>
|
||||
<string name="sign_policy_explainer">Should I approve nostr events automatically or would you like to review them for each app?</string>
|
||||
<string name="finish">Finish</string>
|
||||
<string name="seed_words_title">Your recovery phrase</string>
|
||||
<string name="seed_words_explainer">These words let you recreate your account in case you lose access. Write them down in a safe place</string>
|
||||
<string name="show_seed_words">Show seed words</string>
|
||||
<string name="sign_policy">Sign policy</string>
|
||||
<string name="handle_application_permissions">How to handle this application permissions?</string>
|
||||
<string name="sign_policy_manual_new_app">Manually approve each permission</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recommended for privacy-minded people who would like control over each permission. Choosing this will prompt you set a preference every time you receive a new permission.</string>
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||
<string name="invalid_nostr_connect_uri">Invalid nostr connect URI</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -245,19 +245,19 @@
|
||||
<string name="one_minute">Une minute</string>
|
||||
<string name="five_minutes">Cinq minutes</string>
|
||||
<string name="ten_minutes">Dix minutes</string>
|
||||
<string name="event_kind_9000">Add user to group</string>
|
||||
<string name="event_kind_9001">Remove user from group</string>
|
||||
<string name="event_kind_9002">Edit group metadata</string>
|
||||
<string name="event_kind_9003">Add permission</string>
|
||||
<string name="event_kind_9004">Remove permission</string>
|
||||
<string name="event_kind_9000">Ajouter l\'utilisateur au groupe</string>
|
||||
<string name="event_kind_9001">Supprimer l\'utilisateur du groupe</string>
|
||||
<string name="event_kind_9002">Modifier les métadonnées du groupe</string>
|
||||
<string name="event_kind_9003">Ajouter une permission</string>
|
||||
<string name="event_kind_9004">Supprimer la permission</string>
|
||||
<string name="event_kind_9005">Delete event from group</string>
|
||||
<string name="event_kind_9006">Edit group status</string>
|
||||
<string name="event_kind_9007">Create group</string>
|
||||
<string name="event_kind_9021">Join group</string>
|
||||
<string name="event_kind_39000">Group metadata</string>
|
||||
<string name="event_kind_39001">Group admins</string>
|
||||
<string name="event_kind_39002">Group members</string>
|
||||
<string name="more_info">More information about this permission</string>
|
||||
<string name="event_kind_9007">Créer un groupe</string>
|
||||
<string name="event_kind_9021">Rejoindre le groupe</string>
|
||||
<string name="event_kind_39000">Métadonnées du groupe</string>
|
||||
<string name="event_kind_39001">Admins du groupe</string>
|
||||
<string name="event_kind_39002">Membres du groupe</string>
|
||||
<string name="more_info">Plus d\'informations sur cette autorisation</string>
|
||||
<string name="nip_kind">Nip %1$s</string>
|
||||
<string name="nip_01">Basic protocol flow description</string>
|
||||
<string name="nip_02">Follow List</string>
|
||||
@@ -265,31 +265,31 @@
|
||||
<string name="nip_04">Encrypted Direct Message</string>
|
||||
<string name="nip_05">Mapping Nostr keys to DNS-based internet identifiers</string>
|
||||
<string name="nip_06">Basic key derivation from mnemonic seed phrase</string>
|
||||
<string name="nip_07">window.nostr capability for web browsers</string>
|
||||
<string name="nip_07">Capacité window.nostr pour les navigateurs web</string>
|
||||
<string name="nip_08">Handling Mentions</string>
|
||||
<string name="nip_09">Event Deletion</string>
|
||||
<string name="nip_10">Conventions for clients</string>
|
||||
<string name="nip_11">Relay Information Document</string>
|
||||
<string name="nip_13">Proof of Work</string>
|
||||
<string name="nip_13">Preuve de travail</string>
|
||||
<string name="nip_14">Subject tag in text events</string>
|
||||
<string name="nip_15">Nostr Marketplace (for resilient marketplaces)</string>
|
||||
<string name="nip_17">Private Direct Messages</string>
|
||||
<string name="nip_18">Reposts</string>
|
||||
<string name="nip_17">Messages privés</string>
|
||||
<string name="nip_18">Republications</string>
|
||||
<string name="nip_19">bech32-encoded entities</string>
|
||||
<string name="nip_21">nostr: URI scheme</string>
|
||||
<string name="nip_23">Long-form Content</string>
|
||||
<string name="nip_24">Extra metadata fields and tags</string>
|
||||
<string name="nip_25">Reactions</string>
|
||||
<string name="nip_25">Réactions</string>
|
||||
<string name="nip_26">Delegated Event Signing</string>
|
||||
<string name="nip_27">Text Note References</string>
|
||||
<string name="nip_28">Public Chat</string>
|
||||
<string name="nip_28">Chat public</string>
|
||||
<string name="nip_29">Relay-based Groups</string>
|
||||
<string name="nip_30">Custom Emoji</string>
|
||||
<string name="nip_31">Dealing with Unknown Events</string>
|
||||
<string name="nip_30">Émojis personnalisés</string>
|
||||
<string name="nip_31">Traitement d\'événements inconnus</string>
|
||||
<string name="nip_32">Labeling</string>
|
||||
<string name="nip_34">git stuff</string>
|
||||
<string name="nip_35">Torrents</string>
|
||||
<string name="nip_36">Sensitive Content</string>
|
||||
<string name="nip_36">Contenu Sensible</string>
|
||||
<string name="nip_38">User Statuses</string>
|
||||
<string name="nip_39">External Identities in Profiles</string>
|
||||
<string name="nip_40">Expiration Timestamp</string>
|
||||
@@ -299,22 +299,22 @@
|
||||
<string name="nip_46">Nostr Connect</string>
|
||||
<string name="nip_47">Wallet Connect</string>
|
||||
<string name="nip_48">Proxy Tags</string>
|
||||
<string name="nip_49">Private Key Encryption</string>
|
||||
<string name="nip_49">Chiffrement de la clé privée</string>
|
||||
<string name="nip_50">Search Capability</string>
|
||||
<string name="nip_51">Lists</string>
|
||||
<string name="nip_52">Calendar Events</string>
|
||||
<string name="nip_51">Listes</string>
|
||||
<string name="nip_52">Évènements calendrier</string>
|
||||
<string name="nip_53">Live Activities</string>
|
||||
<string name="nip_54">Wiki</string>
|
||||
<string name="nip_55">Android Signer Application</string>
|
||||
<string name="nip_56">Reporting</string>
|
||||
<string name="nip_57">Lightning Zaps</string>
|
||||
<string name="nip_55">Application Android de signature</string>
|
||||
<string name="nip_56">Signalement</string>
|
||||
<string name="nip_57">Zaps lightning</string>
|
||||
<string name="nip_58">Badges</string>
|
||||
<string name="nip_59">Gift Wrap</string>
|
||||
<string name="nip_65">Relay List Metadata</string>
|
||||
<string name="nip_70">Protected Events</string>
|
||||
<string name="nip_71">Video Events</string>
|
||||
<string name="nip_72">Moderated Communities</string>
|
||||
<string name="nip_75">Zap Goals</string>
|
||||
<string name="nip_72">Communautés modérées</string>
|
||||
<string name="nip_75">Objectifs de Zap</string>
|
||||
<string name="nip_78">Application-specific data</string>
|
||||
<string name="nip_84">Highlights</string>
|
||||
<string name="nip_89">Recommended Application Handlers</string>
|
||||
@@ -322,30 +322,56 @@
|
||||
<string name="nip_92">Media Attachments</string>
|
||||
<string name="nip_94">File Metadata</string>
|
||||
<string name="nip_96">HTTP File Storage Integration</string>
|
||||
<string name="nip_98">HTTP Auth</string>
|
||||
<string name="nip_98">Authentification HTTP</string>
|
||||
<string name="nip_99">Classified Listings</string>
|
||||
<string name="no_information_available_for">No information available for %1$s</string>
|
||||
<string name="nip_not_found">Nip %1$s not found</string>
|
||||
<string name="nip_not_found_for_the_event_kind">Nip not found for the event kind %1$s</string>
|
||||
<string name="use_secret_to_connect_to_the_application">Use secret to connect to the application</string>
|
||||
<string name="add_a_key">Add a key</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="add_your_key">Add your key</string>
|
||||
<string name="sign_policy_basic">Approve basic actions</string>
|
||||
<string name="sign_policy_basic_explainer">Recommended for most people. This will minimize the number of interruptions during your app usage.</string>
|
||||
<string name="sign_policy_manual">Manually approve each app</string>
|
||||
<string name="sign_policy_manual_explainer">Recommended for privacy-minded people who would like control over each app. Choosing this will prompt you set a preference every time you try a new app.</string>
|
||||
<string name="sign_policy_title">Select a signing policy</string>
|
||||
<string name="sign_policy_explainer">Should I approve nostr events automatically or would you like to review them for each app?</string>
|
||||
<string name="finish">Finish</string>
|
||||
<string name="seed_words_title">Your recovery phrase</string>
|
||||
<string name="seed_words_explainer">These words let you recreate your account in case you lose access. Write them down in a safe place</string>
|
||||
<string name="show_seed_words">Show seed words</string>
|
||||
<string name="sign_policy">Sign policy</string>
|
||||
<string name="handle_application_permissions">How to handle this application permissions?</string>
|
||||
<string name="sign_policy_manual_new_app">Manually approve each permission</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recommended for privacy-minded people who would like control over each permission. Choosing this will prompt you set a preference every time you receive a new permission.</string>
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="no_information_available_for">Aucune information disponible pour %1$s</string>
|
||||
<string name="nip_not_found">Nip %1$s introuvable</string>
|
||||
<string name="nip_not_found_for_the_event_kind">Nip introuvable pour le type d\'événement %1$s</string>
|
||||
<string name="use_secret_to_connect_to_the_application">Utiliser un secret pour se connecter à l\'application</string>
|
||||
<string name="add_a_key">Ajouter une clé</string>
|
||||
<string name="next">Suivant</string>
|
||||
<string name="add_your_key">Ajouter votre clé</string>
|
||||
<string name="sign_policy_basic">Approuver les actions de base</string>
|
||||
<string name="sign_policy_basic_explainer">Recommandé pour la plupart des personnes. Cela minimisera le nombre d\'interruptions pendant l\'utilisation de l\'application.</string>
|
||||
<string name="sign_policy_manual">Approuver manuellement chaque application</string>
|
||||
<string name="sign_policy_manual_explainer">Recommandé pour les personnes soucieuses de la vie privée qui souhaitent contrôler chaque application. Choisir ceci vous demandera de définir une préférence chaque fois que vous essayerez une nouvelle application.</string>
|
||||
<string name="sign_policy_title">Sélectionnez une politique de signature</string>
|
||||
<string name="sign_policy_explainer">Devrais-je approuver automatiquement les événements nostr ou souhaitez-vous les examiner pour chaque application ?</string>
|
||||
<string name="finish">Terminer</string>
|
||||
<string name="seed_words_title">Votre phrase de récupération</string>
|
||||
<string name="seed_words_explainer">Ces mots vous permettent de recréer votre compte en cas de perte d\'accès. Notez-les dans un endroit sûr</string>
|
||||
<string name="show_seed_words">Afficher les mots de la seed</string>
|
||||
<string name="sign_policy">Politique de signature</string>
|
||||
<string name="handle_application_permissions">Comment gérer les permissions de cette application ?</string>
|
||||
<string name="sign_policy_manual_new_app">Approuver manuellement chaque permission</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recommandé pour les personnes soucieuses de la vie privée qui voudraient avoir le contrôle de chaque permission. Choisir ceci vous demandera de définir une préférence chaque fois que vous recevez une nouvelle permission.</string>
|
||||
<string name="sign_policy_fully">Je fais entièrement confiance à cette application</string>
|
||||
<string name="sign_policy_fully_explainer">Signer automatiquement chaque requête</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="paste_from_clipboard">Coller depuis le presse-papier</string>
|
||||
<string name="invalid_nostr_connect_uri">URI de connexion nostr invalide</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -235,115 +235,141 @@
|
||||
<string name="requests">solicita</string>
|
||||
<string name="disable_listening_for_new_connections">Desativar escuta de novas conexões</string>
|
||||
<string name="enable_listening_for_new_connections">Permitir escutar por novas conexões</string>
|
||||
<string name="authenticate">Authentication needed to use the app</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="enable_biometrics">Enable biometrics</string>
|
||||
<string name="when_to_ask">When to ask</string>
|
||||
<string name="every_time">Every time</string>
|
||||
<string name="one_minute">One minute</string>
|
||||
<string name="five_minutes">Five minutes</string>
|
||||
<string name="ten_minutes">Ten Minutes</string>
|
||||
<string name="event_kind_9000">Add user to group</string>
|
||||
<string name="event_kind_9001">Remove user from group</string>
|
||||
<string name="event_kind_9002">Edit group metadata</string>
|
||||
<string name="event_kind_9003">Add permission</string>
|
||||
<string name="event_kind_9004">Remove permission</string>
|
||||
<string name="event_kind_9005">Delete event from group</string>
|
||||
<string name="event_kind_9006">Edit group status</string>
|
||||
<string name="event_kind_9007">Create group</string>
|
||||
<string name="event_kind_9021">Join group</string>
|
||||
<string name="event_kind_39000">Group metadata</string>
|
||||
<string name="event_kind_39001">Group admins</string>
|
||||
<string name="event_kind_39002">Group members</string>
|
||||
<string name="more_info">More information about this permission</string>
|
||||
<string name="authenticate">Autenticação necessária para usar o aplicativo</string>
|
||||
<string name="security">Segurança</string>
|
||||
<string name="enable_biometrics">Habilitar biometria</string>
|
||||
<string name="when_to_ask">Quando perguntar</string>
|
||||
<string name="every_time">Sempre</string>
|
||||
<string name="one_minute">Um minuto</string>
|
||||
<string name="five_minutes">Cinco minutos</string>
|
||||
<string name="ten_minutes">Dez minutos</string>
|
||||
<string name="event_kind_9000">Adicionar usuário ao grupo</string>
|
||||
<string name="event_kind_9001">Remover usuário do grupo</string>
|
||||
<string name="event_kind_9002">Editar metadados do grupo</string>
|
||||
<string name="event_kind_9003">Adicionar permissão</string>
|
||||
<string name="event_kind_9004">Remover permissão</string>
|
||||
<string name="event_kind_9005">Excluir evento do grupo</string>
|
||||
<string name="event_kind_9006">Editar status do grupo</string>
|
||||
<string name="event_kind_9007">Criar grupo</string>
|
||||
<string name="event_kind_9021">Entrar no Grupo</string>
|
||||
<string name="event_kind_39000">Metadados do grupo</string>
|
||||
<string name="event_kind_39001">Administradores do grupo</string>
|
||||
<string name="event_kind_39002">Membros do grupo</string>
|
||||
<string name="more_info">Mais informações sobre esta permissão</string>
|
||||
<string name="nip_kind">Nip %1$s</string>
|
||||
<string name="nip_01">Basic protocol flow description</string>
|
||||
<string name="nip_02">Follow List</string>
|
||||
<string name="nip_03">OpenTimestamps Attestations for Events</string>
|
||||
<string name="nip_04">Encrypted Direct Message</string>
|
||||
<string name="nip_05">Mapping Nostr keys to DNS-based internet identifiers</string>
|
||||
<string name="nip_06">Basic key derivation from mnemonic seed phrase</string>
|
||||
<string name="nip_07">window.nostr capability for web browsers</string>
|
||||
<string name="nip_08">Handling Mentions</string>
|
||||
<string name="nip_09">Event Deletion</string>
|
||||
<string name="nip_10">Conventions for clients</string>
|
||||
<string name="nip_11">Relay Information Document</string>
|
||||
<string name="nip_13">Proof of Work</string>
|
||||
<string name="nip_14">Subject tag in text events</string>
|
||||
<string name="nip_15">Nostr Marketplace (for resilient marketplaces)</string>
|
||||
<string name="nip_17">Private Direct Messages</string>
|
||||
<string name="nip_01">Descrição do fluxo de protocolo básico</string>
|
||||
<string name="nip_02">Lista de seguidores</string>
|
||||
<string name="nip_03">Atestados OpenTimestamps para eventos</string>
|
||||
<string name="nip_04">Mensagens diretas criptografadas</string>
|
||||
<string name="nip_05">Mapeando as chaves Nostr para identificadores de internet baseados em DNS</string>
|
||||
<string name="nip_06">Derivação básica da seed phrase</string>
|
||||
<string name="nip_07">window.nostr recurso para navegadores da web</string>
|
||||
<string name="nip_08">Tratamento de Menções</string>
|
||||
<string name="nip_09">Evento de exclusão</string>
|
||||
<string name="nip_10">Convenções para clientes</string>
|
||||
<string name="nip_11">Documento de informação do Relay</string>
|
||||
<string name="nip_13">Prova de Trabalho</string>
|
||||
<string name="nip_14">Tag de assunto em eventos de texto</string>
|
||||
<string name="nip_15">Nostr Marketplace (para mercados resilientes)</string>
|
||||
<string name="nip_17">Mensagens Diretas Privadas</string>
|
||||
<string name="nip_18">Reposts</string>
|
||||
<string name="nip_19">bech32-encoded entities</string>
|
||||
<string name="nip_19">entidades codificadas por bech32</string>
|
||||
<string name="nip_21">nostr: URI scheme</string>
|
||||
<string name="nip_23">Long-form Content</string>
|
||||
<string name="nip_24">Extra metadata fields and tags</string>
|
||||
<string name="nip_25">Reactions</string>
|
||||
<string name="nip_26">Delegated Event Signing</string>
|
||||
<string name="nip_27">Text Note References</string>
|
||||
<string name="nip_28">Public Chat</string>
|
||||
<string name="nip_29">Relay-based Groups</string>
|
||||
<string name="nip_30">Custom Emoji</string>
|
||||
<string name="nip_31">Dealing with Unknown Events</string>
|
||||
<string name="nip_32">Labeling</string>
|
||||
<string name="nip_34">git stuff</string>
|
||||
<string name="nip_23">Conteúdo de longa forma</string>
|
||||
<string name="nip_24">Campos e tags extras de metadados</string>
|
||||
<string name="nip_25">Reações</string>
|
||||
<string name="nip_26">Assinatura de Evento Delegado</string>
|
||||
<string name="nip_27">Referências de notas de texto</string>
|
||||
<string name="nip_28">Chat Público</string>
|
||||
<string name="nip_29">Grupos baseados em Relays</string>
|
||||
<string name="nip_30">Emoji personalizado</string>
|
||||
<string name="nip_31">Lidando com eventos desconhecidos</string>
|
||||
<string name="nip_32">Rotulagem</string>
|
||||
<string name="nip_34">coisas do git</string>
|
||||
<string name="nip_35">Torrents</string>
|
||||
<string name="nip_36">Sensitive Content</string>
|
||||
<string name="nip_38">User Statuses</string>
|
||||
<string name="nip_39">External Identities in Profiles</string>
|
||||
<string name="nip_40">Expiration Timestamp</string>
|
||||
<string name="nip_42">Authentication of clients to relays</string>
|
||||
<string name="nip_44">Versioned Encryption</string>
|
||||
<string name="nip_45">Counting results</string>
|
||||
<string name="nip_36">Conteúdo sensível</string>
|
||||
<string name="nip_38">Status do usuário.</string>
|
||||
<string name="nip_39">Identidades externas em Perfis</string>
|
||||
<string name="nip_40">Data de expiração</string>
|
||||
<string name="nip_42">Autenticação de clientes para relays</string>
|
||||
<string name="nip_44">Criptografia versionada</string>
|
||||
<string name="nip_45">Resultados da contagem</string>
|
||||
<string name="nip_46">Nostr Connect</string>
|
||||
<string name="nip_47">Wallet Connect</string>
|
||||
<string name="nip_48">Proxy Tags</string>
|
||||
<string name="nip_49">Private Key Encryption</string>
|
||||
<string name="nip_50">Search Capability</string>
|
||||
<string name="nip_51">Lists</string>
|
||||
<string name="nip_52">Calendar Events</string>
|
||||
<string name="nip_53">Live Activities</string>
|
||||
<string name="nip_48">Tags Proxy</string>
|
||||
<string name="nip_49">Criptografia de chave privada</string>
|
||||
<string name="nip_50">Capacidade de pesquisa</string>
|
||||
<string name="nip_51">Listas</string>
|
||||
<string name="nip_52">Eventos de calendário</string>
|
||||
<string name="nip_53">Atividades ao vivo</string>
|
||||
<string name="nip_54">Wiki</string>
|
||||
<string name="nip_55">Android Signer Application</string>
|
||||
<string name="nip_56">Reporting</string>
|
||||
<string name="nip_57">Lightning Zaps</string>
|
||||
<string name="nip_58">Badges</string>
|
||||
<string name="nip_55">Aplicativo Android Signer</string>
|
||||
<string name="nip_56">Denúncia</string>
|
||||
<string name="nip_57">Zaps Lightning</string>
|
||||
<string name="nip_58">Emblemas</string>
|
||||
<string name="nip_59">Gift Wrap</string>
|
||||
<string name="nip_65">Relay List Metadata</string>
|
||||
<string name="nip_70">Protected Events</string>
|
||||
<string name="nip_71">Video Events</string>
|
||||
<string name="nip_72">Moderated Communities</string>
|
||||
<string name="nip_75">Zap Goals</string>
|
||||
<string name="nip_78">Application-specific data</string>
|
||||
<string name="nip_84">Highlights</string>
|
||||
<string name="nip_89">Recommended Application Handlers</string>
|
||||
<string name="nip_90">Data Vending Machines</string>
|
||||
<string name="nip_92">Media Attachments</string>
|
||||
<string name="nip_94">File Metadata</string>
|
||||
<string name="nip_96">HTTP File Storage Integration</string>
|
||||
<string name="nip_98">HTTP Auth</string>
|
||||
<string name="nip_99">Classified Listings</string>
|
||||
<string name="no_information_available_for">No information available for %1$s</string>
|
||||
<string name="nip_not_found">Nip %1$s not found</string>
|
||||
<string name="nip_not_found_for_the_event_kind">Nip not found for the event kind %1$s</string>
|
||||
<string name="use_secret_to_connect_to_the_application">Use secret to connect to the application</string>
|
||||
<string name="add_a_key">Add a key</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="add_your_key">Add your key</string>
|
||||
<string name="sign_policy_basic">Approve basic actions</string>
|
||||
<string name="sign_policy_basic_explainer">Recommended for most people. This will minimize the number of interruptions during your app usage.</string>
|
||||
<string name="sign_policy_manual">Manually approve each app</string>
|
||||
<string name="sign_policy_manual_explainer">Recommended for privacy-minded people who would like control over each app. Choosing this will prompt you set a preference every time you try a new app.</string>
|
||||
<string name="sign_policy_title">Select a signing policy</string>
|
||||
<string name="sign_policy_explainer">Should I approve nostr events automatically or would you like to review them for each app?</string>
|
||||
<string name="finish">Finish</string>
|
||||
<string name="seed_words_title">Your recovery phrase</string>
|
||||
<string name="seed_words_explainer">These words let you recreate your account in case you lose access. Write them down in a safe place</string>
|
||||
<string name="show_seed_words">Show seed words</string>
|
||||
<string name="sign_policy">Sign policy</string>
|
||||
<string name="handle_application_permissions">How to handle this application permissions?</string>
|
||||
<string name="sign_policy_manual_new_app">Manually approve each permission</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recommended for privacy-minded people who would like control over each permission. Choosing this will prompt you set a preference every time you receive a new permission.</string>
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="nip_65">Metadados da lista de Relay</string>
|
||||
<string name="nip_70">Eventos protegidos</string>
|
||||
<string name="nip_71">Eventos de Vídeo</string>
|
||||
<string name="nip_72">Comunidades Moderadas</string>
|
||||
<string name="nip_75">Metas do Zap</string>
|
||||
<string name="nip_78">Dados específicos da aplicação</string>
|
||||
<string name="nip_84">Destaques</string>
|
||||
<string name="nip_89">Manipuladores de aplicativos recomendados</string>
|
||||
<string name="nip_90">Máquinas de venda de dados</string>
|
||||
<string name="nip_92">Anexos de mídia</string>
|
||||
<string name="nip_94">Metadados de arquivos</string>
|
||||
<string name="nip_96">Integração de armazenamento de arquivos HTTP</string>
|
||||
<string name="nip_98">Autenticação HTTP</string>
|
||||
<string name="nip_99">Anúncio classificado</string>
|
||||
<string name="no_information_available_for">Nenhuma informação disponível para %1$s</string>
|
||||
<string name="nip_not_found">Nip %1$s não encontrada</string>
|
||||
<string name="nip_not_found_for_the_event_kind">Nip não foi encontrada para o tipo de evento %1$s</string>
|
||||
<string name="use_secret_to_connect_to_the_application">Usar segredo para se conectar ao aplicativo</string>
|
||||
<string name="add_a_key">Adicionar uma chave</string>
|
||||
<string name="next">Próximo</string>
|
||||
<string name="add_your_key">Adicione sua chave</string>
|
||||
<string name="sign_policy_basic">Aprovar ações básicas</string>
|
||||
<string name="sign_policy_basic_explainer">Recomendado para a maioria das pessoas. Isso minimizará o número de interrupções durante o uso do seu aplicativo.</string>
|
||||
<string name="sign_policy_manual">Aprovar manualmente cada app</string>
|
||||
<string name="sign_policy_manual_explainer">Recomendado para pessoas preocupadas com privacidade que gostariam de ter controle sobre cada aplicativo. Escolher isso fará com que você defina uma preferência sempre que testar um novo aplicativo.</string>
|
||||
<string name="sign_policy_title">Selecione uma política de assinatura</string>
|
||||
<string name="sign_policy_explainer">Devo aprovar eventos de nostr automaticamente ou você gostaria de revisá-los para cada aplicativo?</string>
|
||||
<string name="finish">Finalizar</string>
|
||||
<string name="seed_words_title">Sua frase de recuperação</string>
|
||||
<string name="seed_words_explainer">Estas palavras permitem que você recrie sua conta caso você perca o acesso. Anote em um lugar seguro</string>
|
||||
<string name="show_seed_words">Mostrar palavras-chave</string>
|
||||
<string name="sign_policy">Política de assinatura</string>
|
||||
<string name="handle_application_permissions">Como lidar com as permissões desta aplicação?</string>
|
||||
<string name="sign_policy_manual_new_app">Aprovar manualmente cada permissão</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recomendado para pessoas preocupadas com privacidade que gostariam de ter controle sobre cada permissão. Escolher isso fará com que você defina uma preferência toda vez que receber uma nova permissão.</string>
|
||||
<string name="sign_policy_fully">Eu confio totalmente neste aplicativo</string>
|
||||
<string name="sign_policy_fully_explainer">Assine automaticamente cada solicitação</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="paste_from_clipboard">Colar da área de transferência</string>
|
||||
<string name="invalid_nostr_connect_uri">URI de conexão nostr inválida</string>
|
||||
<string name="event_kind_1986">Revisões de Relay</string>
|
||||
<string name="event_kind_1987">Embeddings de IA / Listas de vetores</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">Lista de servidores de usuários</string>
|
||||
<string name="event_kind_24242">Blobs armazenados em mediaservers</string>
|
||||
<string name="event_kind_30040">Cabeçalho de Artigo Modular</string>
|
||||
<string name="event_kind_30041">Conteúdo de Artigo Modular</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -345,4 +345,30 @@
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||
<string name="invalid_nostr_connect_uri">Invalid nostr connect URI</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -348,4 +348,13 @@
|
||||
<string name="sign_policy_fully">完全信任该应用</string>
|
||||
<string name="sign_policy_fully_explainer">自动签署每个请求</string>
|
||||
<string name="event_kind_10050">私信中继</string>
|
||||
<string name="paste_from_clipboard">从剪贴板粘贴</string>
|
||||
<string name="invalid_nostr_connect_uri">无效的 Nostr 连接 URI</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
</resources>
|
||||
|
||||
@@ -1 +1,377 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"><string name="app_name_debug" translatable="false">Amber Debug</string></resources>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## 密钥备份和安全提示
|
||||
\n\n您的账户由私钥保障。私钥是以 **nsec1** 开头的随机长字符串。任何可以访问您的私钥的人都可以使用您的身份发布内容。
|
||||
\n\n- **不要**把你的私钥填进任何您无法信任的网站或软件。
|
||||
\n- Amber 的开发者**永远**不会索要您的私钥。
|
||||
\n- 为了能恢复您的账户**务必**保存好您的私钥安全备份。我们建议使用密码管理器。
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">私钥 (nsec) 已复制到剪贴板</string>
|
||||
<string name="copy_my_secret_key">复制我的私钥</string>
|
||||
<string name="biometric_authentication_failed">身份验证失败</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">将 Nsec ID (您的密码) 复制到剪贴板进行备份</string>
|
||||
<string name="event_pubkey_is_not_equal_to_current_logged_in_user">事件的 pubkey 与当前登录用户不符</string>
|
||||
<string name="signature_copied_to_the_clipboard">签名已复制到剪贴板</string>
|
||||
<string name="reset_permissions">重置权限</string>
|
||||
<string name="logout">注销</string>
|
||||
<string name="logging_out_deletes_all_your_local_information_make_sure_to_have_your_private_keys_backed_up_to_avoid_losing_your_account_do_you_want_to_continue">注销后会删除所有本地信息。为避免账户丢失请确保您已经备份了私钥。还要继续吗?</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="do_you_want_to_reset_all_permissions">确认重置所有权限?</string>
|
||||
<string name="confirm">确认</string>
|
||||
<string name="nsec">Nsec / 私钥</string>
|
||||
<string name="show_password">显示密码</string>
|
||||
<string name="hide_password">隐藏密码</string>
|
||||
<string name="invalid_key">密钥无效</string>
|
||||
<string name="key_is_required">密钥不能为空</string>
|
||||
<string name="login">登录</string>
|
||||
<string name="generate_a_new_key">生成新密钥</string>
|
||||
<string name="backup_keys">备份密钥</string>
|
||||
<string name="read_your_public_key">读取公钥</string>
|
||||
<string name="no_permissions_granted">未授予任何权限</string>
|
||||
<string name="remember_my_choice_and_don_t_ask_again">记住我的选择且不再询问</string>
|
||||
<string name="reject">拒绝</string>
|
||||
<string name="accept">接受</string>
|
||||
<string name="encrypt_nip44">加密 NIP-44</string>
|
||||
<string name="encrypt_nip04">加密 NIP-04</string>
|
||||
<string name="decrypt_nip44">解密 NIP-44</string>
|
||||
<string name="decrypt_nip04">解密 NIP-04</string>
|
||||
<string name="decrypt_zap_event">解密打闪事件</string>
|
||||
<string name="encrypt_decrypt">加密 / 解密</string>
|
||||
<string name="show_details">显示详情</string>
|
||||
<string name="hide_details">隐藏详情</string>
|
||||
<string name="copy_raw_json">原始 JSON</string>
|
||||
<string name="add_new_account">添加新账户</string>
|
||||
<string name="select_account">选择账户</string>
|
||||
<string name="active_account">激活账户</string>
|
||||
<string name="edit_name">编辑名称</string>
|
||||
<string name="copy">"复制"</string>
|
||||
<string name="encrypted_decrypted_data">加解密数据</string>
|
||||
<string name="data_copied_to_the_clipboard">数据已复制到剪贴版</string>
|
||||
<string name="connect_app">连接应用</string>
|
||||
<string name="event_copied_to_the_clipboard">事件已复制到剪贴板</string>
|
||||
<string name="select_deselect_all">全选 / 反选全部</string>
|
||||
<string name="decrypt_content">解密内容</string>
|
||||
<string name="support_development">支持开发</string>
|
||||
<string name="source_code">源代码</string>
|
||||
<string name="show_qr_code">显示二维码</string>
|
||||
<string name="shows_qr_code_with_you_private_key">显示二维码和私钥</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password.
|
||||
\n\nIf you lose your password, you will not be able to recover your key.
|
||||
</string>
|
||||
<string name="ncryptsec_password">密码</string>
|
||||
<string name="password_is_required">需要密码</string>
|
||||
<string name="failed_to_encrypt_key">私钥加密失败</string>
|
||||
<string name="biometric_authentication_failed_explainer">生物识别验证设备所有者身份失败</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">生物识别验证设备所有者身份失败:%1$s</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">加密并复制私钥</string>
|
||||
<string name="delete">删除</string>
|
||||
<string name="permissions">权限</string>
|
||||
<string name="delete_application">删除应用</string>
|
||||
<string name="copy_to_clipboard">复制到剪贴板</string>
|
||||
<string name="nothing_to_approve_yet">暂无批复请求</string>
|
||||
<string name="why_not_explore_your_favorite_nostr_app_a_bit">为什么不去探索一下你喜欢的 Nostr 应用呢?</string>
|
||||
<string name="grant_permissions">授权</string>
|
||||
<string name="adjust">调整</string>
|
||||
<string name="more">%1$s 项其他权限...</string>
|
||||
<string name="and_more">... 及其他更多</string>
|
||||
<string name="would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf">"想要获取权限读取公钥并签署事件"</string>
|
||||
<string name="new_app">新应用</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Connect through Tor with Orbot
|
||||
\n\n1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Start Orbot
|
||||
\n3. In Orbot, check the Socks port. The default uses 9050
|
||||
\n4. If necessary change the port in Orbot
|
||||
\n5. Configure the Socks port in this screen
|
||||
\n6. Press the Activate button to use Orbot as a proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot SOCKS 端口</string>
|
||||
<string name="invalid_port_number">端口无效</string>
|
||||
<string name="use_orbot">启用 Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">断开 Tor/Orbot 连接</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot 设置</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">断开 Tor/Orbot 连接?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">您的数据将立即在常规网络中传输</string>
|
||||
<string name="yes">确认</string>
|
||||
<string name="no">取消</string>
|
||||
<string name="could_not_connect_to_tor">无法连接到 Tor/Orbot</string>
|
||||
<string name="relays">中继</string>
|
||||
<string name="push_notifications">通知推送</string>
|
||||
<string name="direct_connection">直接连接</string>
|
||||
<string name="notification_type">通知类型</string>
|
||||
<string name="select_the_type_of_notification_you_want_to_receive">选择通知推送类型</string>
|
||||
<string name="activity">活动</string>
|
||||
<string name="accepted">已接受</string>
|
||||
<string name="rejected">已拒绝</string>
|
||||
<string name="language">语言</string>
|
||||
<string name="language_description">用于应用界面显示</string>
|
||||
<string name="no_activities_found">未找到活动</string>
|
||||
<string name="home">首页</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="ok">确定</string>
|
||||
<string name="encrypt_data_using_nip_4">使用 NIP-04 加密数据</string>
|
||||
<string name="decrypt_data_using_nip_4">使用 NIP-04 解密数据</string>
|
||||
<string name="decrypt_data_using_nip_44">使用 NIP-44 解密数据</string>
|
||||
<string name="encrypt_data_using_nip_44">使用 NIP-44 加密数据</string>
|
||||
<string name="decrypt_private_zaps">解密私密打闪</string>
|
||||
<string name="event_kind_0">元数据</string>
|
||||
<string name="event_kind_1">短文笔记</string>
|
||||
<string name="event_kind_3">关注</string>
|
||||
<string name="event_kind_4">加密私信</string>
|
||||
<string name="event_kind_5">删除事件</string>
|
||||
<string name="event_kind_6">转发</string>
|
||||
<string name="event_kind_7">回应</string>
|
||||
<string name="event_kind_8">已授予徽章</string>
|
||||
<string name="event_kind_9">群组聊天消息</string>
|
||||
<string name="event_kind_10">群组聊天线程回复</string>
|
||||
<string name="event_kind_11">群组消息</string>
|
||||
<string name="event_kind_12">群组消息回复</string>
|
||||
<string name="event_kind_13">Seal 封装</string>
|
||||
<string name="event_kind_16">通用转发</string>
|
||||
<string name="event_kind_40">创建频道</string>
|
||||
<string name="event_kind_41">频道元数据</string>
|
||||
<string name="event_kind_42">频道消息</string>
|
||||
<string name="event_kind_43">频道消息隐藏</string>
|
||||
<string name="event_kind_44">频道用户静音</string>
|
||||
<string name="event_kind_1021">出价</string>
|
||||
<string name="event_kind_1022">出价确认</string>
|
||||
<string name="event_kind_1040">OpenTimestamps</string>
|
||||
<string name="event_kind_1059">事件包装</string>
|
||||
<string name="event_kind_1063">文件元数据</string>
|
||||
<string name="event_kind_1311">直播聊天消息</string>
|
||||
<string name="event_kind_1971">问题追踪器</string>
|
||||
<string name="event_kind_1984">举报</string>
|
||||
<string name="event_kind_1985">标记</string>
|
||||
<string name="event_kind_4550">社区帖子批准</string>
|
||||
<string name="event_kind_5000_5999">数据作业请求</string>
|
||||
<string name="event_kind_6000_6999">数据作业结果</string>
|
||||
<string name="event_kind_7000">数据作业反馈</string>
|
||||
<string name="event_kind_9000_9030">群组审核事件</string>
|
||||
<string name="event_kind_9041">打闪目标</string>
|
||||
<string name="event_kind">事件类型 %1$s</string>
|
||||
<string name="event_kind_34550">社区定义</string>
|
||||
<string name="event_kind_39000_39009">群组元数据事件</string>
|
||||
<string name="event_kind_9734">打闪请求</string>
|
||||
<string name="event_kind_9735">打闪</string>
|
||||
<string name="event_kind_9802">亮点</string>
|
||||
<string name="event_kind_10000">静音列表</string>
|
||||
<string name="event_kind_10001">置顶列表</string>
|
||||
<string name="event_kind_10002">中继列表元数据</string>
|
||||
<string name="event_kind_10003">书签列表</string>
|
||||
<string name="event_kind_10004">社区列表</string>
|
||||
<string name="event_kind_10005">公共聊天列表</string>
|
||||
<string name="event_kind_10006">被禁中继列表</string>
|
||||
<string name="event_kind_10007">搜索中继列表</string>
|
||||
<string name="event_kind_10009">用户组</string>
|
||||
<string name="event_kind_10015">兴趣列表</string>
|
||||
<string name="event_kind_10030">用户表情列表</string>
|
||||
<string name="event_kind_10096">文件服务器列表</string>
|
||||
<string name="event_kind_13194">钱包信息</string>
|
||||
<string name="event_kind_21000">Lightning Pub RPC</string>
|
||||
<string name="event_kind_22242">客户端验证</string>
|
||||
<string name="event_kind_23194">钱包请求</string>
|
||||
<string name="event_kind_23195">钱包响应</string>
|
||||
<string name="event_kind_24133">远程签名请求</string>
|
||||
<string name="event_kind_27235">HTTP 认证</string>
|
||||
<string name="event_kind_30000">关注合集</string>
|
||||
<string name="event_kind_30001">通用列表</string>
|
||||
<string name="event_kind_30002">中继合集</string>
|
||||
<string name="event_kind_30003">书签合集</string>
|
||||
<string name="event_kind_30004">策展合集</string>
|
||||
<string name="event_kind_30008">资料页徽章</string>
|
||||
<string name="event_kind_30009">徽章定义</string>
|
||||
<string name="event_kind_30015">兴趣(标签)合集</string>
|
||||
<string name="event_kind_30017">创建或更新市场摊位</string>
|
||||
<string name="event_kind_30018">创建或更新市场产品</string>
|
||||
<string name="event_kind_30019">市场 UI/UX</string>
|
||||
<string name="event_kind_30020">创建或更新市场产品出售形式为拍卖</string>
|
||||
<string name="event_kind_30023">长篇内容</string>
|
||||
<string name="event_kind_30024">长篇内容草稿</string>
|
||||
<string name="event_kind_30030"></string>
|
||||
<string name="event_kind_30063">发行产物合集</string>
|
||||
<string name="event_kind_30078">特定应用数据</string>
|
||||
<string name="event_kind_30311">直播事件</string>
|
||||
<string name="event_kind_30315">状态</string>
|
||||
<string name="event_kind_30402">分类列表</string>
|
||||
<string name="event_kind_30403">分类列表草稿</string>
|
||||
<string name="event_kind_31234">通用草稿事件</string>
|
||||
<string name="event_kind_31922">日期基底日历事件</string>
|
||||
<string name="event_kind_31923">时间基底日历事件</string>
|
||||
<string name="event_kind_31924"> 日历</string>
|
||||
<string name="event_kind_31925">日历事件回应</string>
|
||||
<string name="event_kind_31989">事件处理推荐</string>
|
||||
<string name="event_kind_31990">事件处理方信息</string>
|
||||
<string name="connect">连接</string>
|
||||
<string name="wants_you_to_sign_a">请求签署 %1$s</string>
|
||||
<string name="wants_you_to">请求进行 %1$s</string>
|
||||
<string name="sign">签署 %1$s</string>
|
||||
<string name="sign_message">签署消息</string>
|
||||
<string name="content">内容</string>
|
||||
<string name="checking_connectivity">正在检查网络连通性...</string>
|
||||
<string name="amber_is_running_in_background">Amber 正在后台运行</string>
|
||||
<string name="bunker_notifications">Bunker 通知</string>
|
||||
<string name="notifications_for_approving_or_rejecting_bunker_requests">Bunker 请求核查通知</string>
|
||||
<string name="new_event_to_sign">签署新事件</string>
|
||||
<string name="event_kind_10013">私人发件箱中继列表</string>
|
||||
<string name="remove_permission">移除权限</string>
|
||||
<string name="connect_through_your_orbot_setup">通过您的 Orbot 设置进行连接</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="logs">日志</string>
|
||||
<string name="clear_logs">清理日志</string>
|
||||
<string name="default_relays">默认中继</string>
|
||||
<string name="distributors">提供方</string>
|
||||
<string name="select_distributor">选择通知推送服务的提供方</string>
|
||||
<string name="select_push_server">选择统一推送应用</string>
|
||||
<string name="push_server_install_app">通知推送设置</string>
|
||||
<string name="push_server_install_app_description"> To receive push notifications, install any app that supports [Unified Push](https://unifiedpush.org/), such as [Nfty](https://ntfy.sh/).
|
||||
After installing, select the app you want to use in the Settings.
|
||||
</string>
|
||||
<string name="push_server_uses_app_explainer">使用 %1$s</string>
|
||||
<string name="push_server_title">通知推送</string>
|
||||
<string name="push_server_explainer">从已安装的 UnifiedPush 应用选择</string>
|
||||
<string name="quick_action_dont_show_again_button">不再显示</string>
|
||||
<string name="error_dialog_button_ok">确认</string>
|
||||
<string name="push_server_none">无</string>
|
||||
<string name="push_server_none_explainer">禁用通知推送</string>
|
||||
<string name="push_server">推送服务器</string>
|
||||
<string name="no_distributors_found">未找到通知推送提供方。\n请安装类似 ntfy 这样的提供方。</string>
|
||||
<string name="requests">请求</string>
|
||||
<string name="disable_listening_for_new_connections">禁用监听新连接</string>
|
||||
<string name="enable_listening_for_new_connections">启用监听新连接</string>
|
||||
<string name="authenticate">使用应用需要身份验证</string>
|
||||
<string name="security">安全性</string>
|
||||
<string name="enable_biometrics">启用生物识别解锁</string>
|
||||
<string name="when_to_ask">解锁请求</string>
|
||||
<string name="every_time">每次</string>
|
||||
<string name="one_minute">一分钟</string>
|
||||
<string name="five_minutes">五分钟</string>
|
||||
<string name="ten_minutes">十分钟</string>
|
||||
<string name="event_kind_9000">添加用户到群组</string>
|
||||
<string name="event_kind_9001">从群组中移除用户</string>
|
||||
<string name="event_kind_9002">编辑群组信息</string>
|
||||
<string name="event_kind_9003">添加权限</string>
|
||||
<string name="event_kind_9004">移除权限</string>
|
||||
<string name="event_kind_9005">从群组中删除事件</string>
|
||||
<string name="event_kind_9006">编辑群组状态</string>
|
||||
<string name="event_kind_9007">创建群组</string>
|
||||
<string name="event_kind_9021">加入群组</string>
|
||||
<string name="event_kind_39000">群组元数据</string>
|
||||
<string name="event_kind_39001">群组管理员</string>
|
||||
<string name="event_kind_39002">群组成员</string>
|
||||
<string name="more_info">更多关于该权限的信息</string>
|
||||
<string name="nip_kind">NIP-%1$s</string>
|
||||
<string name="nip_01">基本协议流程描述</string>
|
||||
<string name="nip_02">关注列表</string>
|
||||
<string name="nip_03">事件的 OpenTimestamps</string>
|
||||
<string name="nip_04">加密私信</string>
|
||||
<string name="nip_05">将 Nostr 公钥映射至 DNS 的标识符</string>
|
||||
<string name="nip_06">从助记词导出基本密钥</string>
|
||||
<string name="nip_07">网络浏览器的 window.nostr 功能</string>
|
||||
<string name="nip_08">提及处理</string>
|
||||
<string name="nip_09">事件删除 </string>
|
||||
<string name="nip_10">客户端规约</string>
|
||||
<string name="nip_11">中继描述资料</string>
|
||||
<string name="nip_13">工作量证明</string>
|
||||
<string name="nip_14">短文本事件中的主题标签</string>
|
||||
<string name="nip_15">Nostr 市场</string>
|
||||
<string name="nip_17">私信</string>
|
||||
<string name="nip_18">转发</string>
|
||||
<string name="nip_19">bech32 编码实体</string>
|
||||
<string name="nip_21">nostr: URI scheme</string>
|
||||
<string name="nip_23">长篇内容</string>
|
||||
<string name="nip_24">其他元数据字段</string>
|
||||
<string name="nip_25">回应</string>
|
||||
<string name="nip_26">委托事件签署</string>
|
||||
<string name="nip_27">引用</string>
|
||||
<string name="nip_28">公共聊天室</string>
|
||||
<string name="nip_29">基于中继的群聊</string>
|
||||
<string name="nip_30">自定义表情</string>
|
||||
<string name="nip_31">未知事件处理</string>
|
||||
<string name="nip_32">标记</string>
|
||||
<string name="nip_34">git 相关事件</string>
|
||||
<string name="nip_35">Torrents</string>
|
||||
<string name="nip_36">敏感内容</string>
|
||||
<string name="nip_38">用户状态</string>
|
||||
<string name="nip_39">资料页中的外部身份</string>
|
||||
<string name="nip_40">过期时间戳</string>
|
||||
<string name="nip_42">客户端中继认证</string>
|
||||
<string name="nip_44">版本控制化加密</string>
|
||||
<string name="nip_45">计数结果</string>
|
||||
<string name="nip_46">远程签名</string>
|
||||
<string name="nip_47">钱包集成连接</string>
|
||||
<string name="nip_48">代理标签</string>
|
||||
<string name="nip_49">私钥加密</string>
|
||||
<string name="nip_50">搜索功能</string>
|
||||
<string name="nip_51">列表</string>
|
||||
<string name="nip_52">日历事件</string>
|
||||
<string name="nip_53">直播活动</string>
|
||||
<string name="nip_54">百科</string>
|
||||
<string name="nip_55">Android 签名器</string>
|
||||
<string name="nip_56">举报</string>
|
||||
<string name="nip_57">打闪</string>
|
||||
<string name="nip_58">徽章</string>
|
||||
<string name="nip_59">事件包装</string>
|
||||
<string name="nip_65">中继列表元数据</string>
|
||||
<string name="nip_70">受保护事件</string>
|
||||
<string name="nip_71">视频事件</string>
|
||||
<string name="nip_72">受管理社区</string>
|
||||
<string name="nip_75">打闪目标</string>
|
||||
<string name="nip_78">应用数据</string>
|
||||
<string name="nip_84">亮点</string>
|
||||
<string name="nip_89">推荐事件处理应用</string>
|
||||
<string name="nip_90">数据售货机</string>
|
||||
<string name="nip_92">媒体附件</string>
|
||||
<string name="nip_94">文件元数据</string>
|
||||
<string name="nip_96">HTTP 文件存储集成</string>
|
||||
<string name="nip_98">HTTP 认证</string>
|
||||
<string name="nip_99">分类列表</string>
|
||||
<string name="no_information_available_for">没有关于 %1 的信息。</string>
|
||||
<string name="nip_not_found">未找到 NIP-%1$s</string>
|
||||
<string name="nip_not_found_for_the_event_kind">找不到事件类型 %1$s 的 NIP</string>
|
||||
<string name="use_secret_to_connect_to_the_application">使用机密连接到应用</string>
|
||||
<string name="add_a_key">添加密钥</string>
|
||||
<string name="next">下一步</string>
|
||||
<string name="add_your_key">添加你的密钥</string>
|
||||
<string name="sign_policy_basic">自动批复基础请求</string>
|
||||
<string name="sign_policy_basic_explainer">推荐用于大多数人。这将最大限度地减少在使用 Nostr 应用过程中的中断次数。</string>
|
||||
<string name="sign_policy_manual">手动批复每个应用</string>
|
||||
<string name="sign_policy_manual_explainer">推荐用于想要掌控每个应用的隐私优先人士。该选项将会要求手动调整新应用的每一个权限偏好。</string>
|
||||
<string name="sign_policy_title">选择签署策略</string>
|
||||
<string name="sign_policy_explainer">自动批复或手动审查每一个应用?</string>
|
||||
<string name="finish">完成</string>
|
||||
<string name="seed_words_title">恢复口令</string>
|
||||
<string name="seed_words_explainer">这些单词能够在账户丢失时找回密钥。请在安全的位置记下它们。</string>
|
||||
<string name="show_seed_words">显示种子助记词</string>
|
||||
<string name="sign_policy">签署策略</string>
|
||||
<string name="handle_application_permissions">如何处理该应用的权限?</string>
|
||||
<string name="sign_policy_manual_new_app">手动批复每项权限</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">推荐用于想要掌控每项权限的隐私优先人士。该选项将会要求手动调整每个新权限请求的偏好。</string>
|
||||
<string name="sign_policy_fully">完全信任该应用</string>
|
||||
<string name="sign_policy_fully_explainer">自动签署每个请求</string>
|
||||
<string name="event_kind_10050">私信中继</string>
|
||||
<string name="paste_from_clipboard">从剪贴板粘贴</string>
|
||||
<string name="invalid_nostr_connect_uri">无效的 Nostr 连接 URI</string>
|
||||
<string name="event_kind_1986">中继评价</string>
|
||||
<string name="event_kind_1987">AI嵌入或向量列表</string>
|
||||
<string name="event_kind_9467">TIDAL 登录</string>
|
||||
<string name="event_kind_10063">用户服务器列表</string>
|
||||
<string name="event_kind_24242">储存在媒体服务器中的 Blob</string>
|
||||
<string name="event_kind_30040">模块化文章头部</string>
|
||||
<string name="event_kind_30041">模块化文章内容</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -355,4 +355,30 @@
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||
<string name="invalid_nostr_connect_uri">Invalid nostr connect URI</string>
|
||||
<string name="event_kind_1986">Relay reviews</string>
|
||||
<string name="event_kind_1987">AI Embeddings / Vector lists</string>
|
||||
<string name="event_kind_9467">Tidal login</string>
|
||||
<string name="event_kind_10063">User server list</string>
|
||||
<string name="event_kind_24242">Blobs stored on mediaservers</string>
|
||||
<string name="event_kind_30040">Modular Article Header</string>
|
||||
<string name="event_kind_30041">Modular Article Content</string>
|
||||
<string name="push_notifications_explainer">Using push notifications is significantly slower and less private</string>
|
||||
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
|
||||
<string name="notification_settings">Notification Settings</string>
|
||||
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
|
||||
<string name="relay">Relay</string>
|
||||
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
|
||||
<string name="auth_required_message">Relays that require auth are not supported</string>
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
|
||||
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
|
||||
<string name="failed_to_send_event">Failed to send event to relay</string>
|
||||
</resources>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -19,8 +19,13 @@
|
||||
<service
|
||||
android:name=".service.ConnectivityService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"></service>
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Run a foreground service to keep the connection to the relays active"
|
||||
/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.Notifications"
|
||||
|
||||
@@ -23,7 +23,9 @@ package com.greenart7c3.nostrsigner.service
|
||||
import android.util.Log
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.greenart7c3.nostrsigner.AccountInfo
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
|
||||
import com.greenart7c3.nostrsigner.ui.NotificationType
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.tasks.await
|
||||
@@ -34,7 +36,7 @@ object PushNotificationUtils {
|
||||
|
||||
suspend fun init(accounts: List<AccountInfo>) =
|
||||
with(Dispatchers.IO) {
|
||||
if (hasInit) {
|
||||
if (hasInit || NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
|
||||
return@with
|
||||
}
|
||||
// get user notification token provided by firebase
|
||||
|
||||
+3
-3
@@ -12,11 +12,11 @@ files:
|
||||
cs: cs
|
||||
de: de
|
||||
eo: eo
|
||||
es: es
|
||||
es-ES: es
|
||||
fa: fa
|
||||
fr: fr
|
||||
hu: hu
|
||||
in: in
|
||||
id: in
|
||||
ja: ja
|
||||
nl: nl
|
||||
ru: ru
|
||||
@@ -24,5 +24,5 @@ files:
|
||||
th: th
|
||||
tr: tr
|
||||
uk: uk
|
||||
zh: zh
|
||||
zh-CN: zh
|
||||
sw: sw
|
||||
|
||||
@@ -8,8 +8,8 @@ core = "3.5.3"
|
||||
coreKtx = "1.13.1"
|
||||
coreSplashscreen = "1.0.1"
|
||||
espressoCore = "3.6.1"
|
||||
firebaseBom = "33.1.2"
|
||||
firebaseMessaging = "24.0.0"
|
||||
firebaseBom = "33.2.0"
|
||||
firebaseMessaging = "24.0.1"
|
||||
jna = "5.14.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
@@ -17,19 +17,19 @@ lifecycle_version = "2.8.4"
|
||||
material3 = "1.2.1"
|
||||
mockk = "1.13.12"
|
||||
nav_version = "2.7.7"
|
||||
quartz = "0.89.9"
|
||||
quartz = "0.90.3"
|
||||
compose_ui = "1.6.8"
|
||||
richtextUi = "077a2cde64"
|
||||
roomKtx = "2.6.1"
|
||||
securityCryptoKtx = "1.1.0-alpha06"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
kotlin = "2.0.10"
|
||||
kotlin = "2.0.20"
|
||||
workRuntimeKtx = "2.9.1"
|
||||
agp = "8.5.2"
|
||||
agp = "8.6.0"
|
||||
ktlint = "12.1.1"
|
||||
gms = "4.4.2"
|
||||
ksp = "2.0.10-1.0.24"
|
||||
ksp = "2.0.20-1.0.24"
|
||||
|
||||
[libraries]
|
||||
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||
|
||||
Reference in New Issue
Block a user