Refactor permission management and UI components

- Add `updateExpiredPermissions` to `ApplicationDao` to reset expired temporary permissions.
- Implement `AmberToggles` and `ToggleOption` reusable UI components.
- Refactor `EditPermission` to use the new toggle components and ensure expired permissions are updated on load.
- Update `ConnectivityService` to periodically clear expired permissions across all accounts.
- Clean up redundant build flavor checks and logic in `ConnectivityService`.
This commit is contained in:
greenart7c3
2026-01-12 08:15:06 -03:00
parent c455961bc2
commit a7d93697fd
5 changed files with 232 additions and 211 deletions
@@ -42,7 +42,10 @@ interface ApplicationDao {
@Transaction
suspend fun getBySecret(secret: String): ApplicationWithPermissions?
@Query("SELECT * FROM applicationPermission WHERE pkKey = :key and rememberType = 4")
@Query("UPDATE applicationPermission set acceptUntil = 0, rejectUntil = 0, rememberType = 0 where (acceptUntil < :time OR rejectUntil < :time) AND rememberType <> 4")
fun updateExpiredPermissions(time: Long)
@Query("SELECT * FROM applicationPermission WHERE pkKey = :key")
suspend fun getAllByKey(key: String): List<ApplicationPermissionsEntity>
@Query("SELECT `key`, `name` FROM application WHERE name <> ''")
@@ -11,6 +11,7 @@ import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.database.LogEntity
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.Timer
import java.util.TimerTask
import kotlinx.coroutines.CoroutineScope
@@ -109,11 +110,10 @@ class ConnectivityService : Service() {
timer.schedule(
object : TimerTask() {
override fun run() {
if (Amber.instance.settings.killSwitch.value) return
scope.launch {
LocalPreferences.allSavedAccounts(Amber.instance).forEach { accountInfo ->
val now = System.currentTimeMillis() / 1000
Amber.instance.getDatabase(accountInfo.npub).dao().updateExpiredPermissions(TimeUtils.now())
val deleted = Amber.instance.getDatabase(accountInfo.npub).dao().deleteOldApplications(now)
if (deleted > 0) {
Amber.instance.getLogDatabase(accountInfo.npub).dao().insertLog(
@@ -125,9 +125,6 @@ class ConnectivityService : Service() {
time = System.currentTimeMillis(),
),
)
if (!BuildFlavorChecker.isOfflineFlavor()) {
Amber.instance.notificationSubscription.updateFilter()
}
}
}
}
@@ -135,8 +132,11 @@ class ConnectivityService : Service() {
if (BuildFlavorChecker.isOfflineFlavor()) {
return
}
if (Amber.instance.settings.killSwitch.value) return
scope.launch {
Amber.instance.notificationSubscription.updateFilter()
if (!Amber.instance.client.isActive()) {
Amber.instance.client.connect()
}
@@ -1,23 +1,14 @@
package com.greenart7c3.nostrsigner.ui
import android.content.ClipData
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -39,7 +30,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ClipEntry
@@ -62,6 +52,8 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.ui.actions.RemoveAllPermissionsDialog
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.AmberToggles
import com.greenart7c3.nostrsigner.ui.components.ToggleOption
import com.greenart7c3.nostrsigner.ui.theme.orange
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
@@ -98,6 +90,7 @@ fun EditPermission(
LaunchedEffect(selectedPackage) {
val result = withContext(Dispatchers.IO) {
val dao = Amber.instance.getDatabase(account.npub).dao()
dao.updateExpiredPermissions(TimeUtils.now())
val perms = dao.getAllByKey(selectedPackage)
.sortedBy { "${it.type}-${it.kind}" }
@@ -316,9 +309,7 @@ fun PermissionRow(
}
}
val fixedSegmentWidth = 55.dp
val padding = 2.dp
val totalWidth = (fixedSegmentWidth * 3) + (padding * 2)
val totalWidthRememberType = (fixedSegmentWidth * 4) + (padding * 2)
var optionIndex by remember {
if (permission.acceptUntil > 0) {
mutableIntStateOf(0)
@@ -352,215 +343,125 @@ fun PermissionRow(
overflow = TextOverflow.Ellipsis,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
AmberToggles(
count = 3,
selectedIndex = optionIndex,
) {
Box(
modifier = Modifier
.width(totalWidth)
.height(32.dp)
.clip(RoundedCornerShape(6.dp))
.background(MaterialTheme.colorScheme.background)
.padding(padding),
) {
val indicatorOffset by animateDpAsState(
targetValue = fixedSegmentWidth * optionIndex,
animationSpec = tween(durationMillis = 250),
label = "indicatorOffset",
)
ToggleOption(
text = "Allow",
isSelected = optionIndex == 0,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
optionIndex = 0
Box(
modifier = Modifier
.offset(x = indicatorOffset)
.width(fixedSegmentWidth)
.fillMaxHeight()
.clip(RoundedCornerShape(4.dp))
.background(
MaterialTheme.colorScheme.primary,
),
)
Row(modifier = Modifier.fillMaxSize()) {
ToggleOption(
text = "Allow",
isSelected = optionIndex == 0,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
optionIndex = 0
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
ToggleOption(
text = "Deny",
isSelected = optionIndex == 1,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
optionIndex = 1
},
)
ToggleOption(
text = "Deny",
isSelected = optionIndex == 1,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
optionIndex = 1
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
ToggleOption(
text = "Ask",
isSelected = optionIndex == 2,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
optionIndex = 2
},
)
ToggleOption(
text = "Ask",
isSelected = optionIndex == 2,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
optionIndex = 2
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
}
}
},
)
}
if (optionIndex != 2) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.width(totalWidthRememberType)
.height(32.dp)
.clip(RoundedCornerShape(6.dp))
.background(MaterialTheme.colorScheme.background)
.padding(padding),
AmberToggles(
selectedIndex = rememberTypeIndex,
count = 4,
content = {
ToggleOption(
text = "Always",
isSelected = rememberTypeIndex == 0,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 0
) {
val indicatorOffset by animateDpAsState(
targetValue = fixedSegmentWidth * rememberTypeIndex,
animationSpec = tween(durationMillis = 250),
label = "indicatorOffset",
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
Box(
modifier = Modifier
.offset(x = indicatorOffset)
.width(fixedSegmentWidth)
.fillMaxHeight()
.clip(RoundedCornerShape(4.dp))
.background(
MaterialTheme.colorScheme.primary,
),
ToggleOption(
text = "1m",
isSelected = rememberTypeIndex == 1,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 1
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
Row(
modifier = Modifier.fillMaxSize(),
) {
ToggleOption(
text = "Always",
isSelected = rememberTypeIndex == 0,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 0
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
ToggleOption(
text = "1m",
isSelected = rememberTypeIndex == 1,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 1
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
ToggleOption(
text = "5m",
isSelected = rememberTypeIndex == 2,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 2
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
ToggleOption(
text = "10m",
isSelected = rememberTypeIndex == 3,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 3
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
}
}
}
ToggleOption(
text = "5m",
isSelected = rememberTypeIndex == 2,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 2
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
ToggleOption(
text = "10m",
isSelected = rememberTypeIndex == 3,
modifier = Modifier.width(fixedSegmentWidth),
onClick = {
rememberTypeIndex = 3
onSetPermission(
optionIndex,
rememberTypeIndex,
permission,
onToggle,
)
},
)
},
)
}
}
}
@Composable
private fun ToggleOption(
text: String,
isSelected: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
val textColor by animateColorAsState(
targetValue = if (isSelected) Color.Black else MaterialTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(durationMillis = 200),
label = "textColor",
)
Box(
modifier = modifier
.fillMaxHeight()
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = textColor,
)
}
}
@Composable
fun RelayCard(
modifier: Modifier = Modifier,
@@ -0,0 +1,72 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun AmberToggles(
selectedIndex: Int,
count: Int,
content: @Composable RowScope.() -> Unit,
) {
val fixedSegmentWidth = 55.dp
val padding = 2.dp
val totalWidth = (fixedSegmentWidth * count) + (padding * 2)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.width(totalWidth)
.height(32.dp)
.clip(RoundedCornerShape(6.dp))
.background(MaterialTheme.colorScheme.background)
.padding(padding),
) {
val indicatorOffset by animateDpAsState(
targetValue = fixedSegmentWidth * selectedIndex,
animationSpec = tween(durationMillis = 250),
label = "indicatorOffset",
)
Box(
modifier = Modifier
.offset(x = indicatorOffset)
.width(fixedSegmentWidth)
.fillMaxHeight()
.clip(RoundedCornerShape(4.dp))
.background(
MaterialTheme.colorScheme.primary,
),
)
Row(modifier = Modifier.fillMaxSize()) {
this
.content()
}
}
}
}
@@ -0,0 +1,45 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun ToggleOption(
text: String,
isSelected: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
val textColor by animateColorAsState(
targetValue = if (isSelected) Color.Black else MaterialTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(durationMillis = 200),
label = "textColor",
)
Box(
modifier = modifier
.fillMaxHeight()
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = textColor,
)
}
}