Created ActivitiesScreen

This commit is contained in:
greenart7c3
2025-06-13 07:32:28 -03:00
parent 2745d4b71d
commit ab78a0aaec
8 changed files with 358 additions and 96 deletions
@@ -162,6 +162,9 @@ interface ApplicationDao {
@Query("SELECT * FROM history2 where pkKey = :pk ORDER BY time DESC")
fun getAllHistory(pk: String): Flow<List<HistoryEntity2>>
@Query("SELECT * FROM history2 ORDER BY time DESC")
fun getAllHistory(): Flow<List<HistoryEntity2>>
@Insert
@Transaction
suspend fun insertLog(logEntity: LogEntity)
@@ -0,0 +1,206 @@
package com.greenart7c3.nostrsigner.ui
import androidx.compose.foundation.layout.Arrangement
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.database.AppDatabase
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.supportedKindNumbers
import com.greenart7c3.nostrsigner.service.ApplicationNameCache
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.components.SimpleSearchBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActivitiesScreen(
modifier: Modifier,
paddingValues: PaddingValues,
topPadding: Dp,
account: Account,
) {
val database = Amber.instance.getDatabase(account.npub)
val activities = database.applicationDao().getAllHistory().collectAsStateWithLifecycle(emptyList())
val context = LocalContext.current
// State for the search query
var searchQuery by remember { mutableStateOf("") }
// Filtered activities based on the search query
var filteredActivities = activities.value.filter { activity ->
if (searchQuery.isEmpty()) {
true
} else {
val permission = Permission(activity.type.toLowerCase(Locale.current), activity.kind)
permission.toLocalizedString(context, true).contains(searchQuery, ignoreCase = true)
}
}
val textFieldState by remember { mutableStateOf(TextFieldState(initialText = searchQuery)) }
Column(
modifier = modifier.padding(top = topPadding),
) {
SimpleSearchBar(
modifier = Modifier
.padding(start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr), end = paddingValues.calculateRightPadding(LayoutDirection.Ltr))
.fillMaxWidth(),
textFieldState = textFieldState,
onSearch = {
searchQuery = it
},
searchResults = supportedKindNumbers.map { it.toLocalizedString(context, true) },
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr),
end = paddingValues.calculateRightPadding(LayoutDirection.Ltr),
bottom = paddingValues.calculateBottomPadding(),
),
) {
item {
if (filteredActivities.isEmpty()) {
Text(
stringResource(R.string.no_activities_found),
Modifier
.fillMaxWidth()
.padding(8.dp),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
fontSize = 18.sp,
)
}
}
items(filteredActivities) { activity ->
val permission =
Permission(
activity.type.toLowerCase(Locale.current),
activity.kind,
)
Column {
Row(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.fillMaxWidth(0.9f),
verticalArrangement = Arrangement.Center,
) {
ApplicationName(
key = activity.pkKey,
accepted = activity.accepted,
database = database,
account = account,
)
Text(
text = if (permission.type == "connect") stringResource(R.string.connect) else permission.toLocalizedString(context),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (activity.accepted) Color.Unspecified else Color.Gray,
)
Text(
modifier = Modifier.padding(top = 4.dp, bottom = 16.dp),
text = TimeUtils.formatLongToCustomDateTimeWithSeconds(activity.time * 1000),
fontSize = 16.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = Color.Gray,
)
}
Icon(
if (activity.accepted) Icons.Default.Check else Icons.Default.Close,
contentDescription = if (activity.accepted) stringResource(R.string.accepted) else stringResource(R.string.rejected),
tint = if (activity.accepted) Color(0xFF1D8802) else Color(0xFFFF6B00),
modifier = Modifier.padding(start = 10.dp, top = 4.dp, bottom = 16.dp),
)
}
Spacer(Modifier.weight(1f))
HorizontalDivider(
color = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
@Composable
fun ApplicationName(
key: String,
accepted: Boolean,
database: AppDatabase,
account: Account,
) {
var name by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
if (ApplicationNameCache.names["${account.npub.toShortenHex()}-$key"] == null) {
val app = database.applicationDao().getByKey(key)
app?.let {
name = it.application.name
ApplicationNameCache.names["${account.npub.toShortenHex()}-$key"] = it.application.name
}
} else {
ApplicationNameCache.names["${account.npub.toShortenHex()}-$key"]?.let {
name = it
}
}
}
}
Text(
modifier = Modifier.padding(top = 16.dp),
text = name.ifBlank { key.toShortenHex() },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (accepted) Color.Unspecified else Color.Gray,
fontWeight = FontWeight.Bold,
)
}
@@ -39,10 +39,11 @@ import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.navigation.Route
@Composable
fun PermissionsScreen(
fun ApplicationsScreen(
modifier: Modifier,
account: Account,
navController: NavController,
@@ -125,6 +126,14 @@ fun PermissionsScreen(
},
)
} else {
AmberButton(
modifier = Modifier.padding(top = 20.dp),
onClick = {
navController.navigate(Route.Activities.route)
},
text = stringResource(R.string.activity),
)
applications.value.forEach { applicationWithHistory ->
Row(
modifier = Modifier
@@ -383,7 +383,7 @@ fun MainScreen(
content = {
val scrollState = rememberScrollState()
PermissionsScreen(
ApplicationsScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding)
@@ -647,6 +647,26 @@ fun MainScreen(
},
)
composable(
Route.Activities.route,
content = {
ActivitiesScreen(
account = account,
paddingValues = PaddingValues(
top = padding.calculateTopPadding() + (verticalPadding * 1.5f),
bottom = padding.calculateBottomPadding(),
start = padding.calculateStartPadding(LayoutDirection.Ltr) + verticalPadding,
end = padding.calculateEndPadding(LayoutDirection.Ltr) + verticalPadding,
),
topPadding = padding.calculateTopPadding() + (verticalPadding * 1.5f),
modifier =
Modifier
.fillMaxSize()
.imePadding(),
)
},
)
composable(
Route.RelayLogScreen.route,
arguments = listOf(navArgument("url") { type = NavType.StringType }),
@@ -1,15 +1,10 @@
package com.greenart7c3.nostrsigner.ui.actions
import android.view.ViewTreeObserver
import androidx.compose.foundation.background
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.PaddingValues
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
@@ -22,26 +17,18 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
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.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
@@ -51,8 +38,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
@@ -60,85 +45,7 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.supportedKindNumbers
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleSearchBar(
textFieldState: TextFieldState,
onSearch: (String) -> Unit,
searchResults: List<String>,
modifier: Modifier = Modifier,
) {
// Controls expansion state of the search bar
var expanded by rememberSaveable { mutableStateOf(false) }
var localSearchResults by remember { mutableStateOf(searchResults) }
val view = LocalView.current
val viewTreeObserver = view.viewTreeObserver
DisposableEffect(viewTreeObserver) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
val isKeyboardOpen = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) != false
if (!isKeyboardOpen) {
onSearch(textFieldState.text.toString())
expanded = false
}
}
viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
}
Box(
modifier
.semantics { isTraversalGroup = true },
) {
SearchBar(
windowInsets = WindowInsets(0),
inputField = {
SearchBarDefaults.InputField(
modifier = modifier
.fillMaxWidth(),
query = textFieldState.text.toString(),
onQueryChange = {
textFieldState.edit { replace(0, length, it) }
localSearchResults = searchResults.filter { result ->
result.contains(textFieldState.text.toString(), ignoreCase = true)
}
},
onSearch = {
onSearch(textFieldState.text.toString())
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Search") },
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
// Display search results in a scrollable column
LazyColumn {
items(localSearchResults) { result ->
ListItem(
headlineContent = { Text(result) },
modifier = Modifier
.clickable {
textFieldState.edit { replace(0, length, result) }
onSearch(textFieldState.text.toString())
expanded = false
}
.background(Color.Red)
.fillMaxWidth(),
)
}
}
}
}
}
import com.greenart7c3.nostrsigner.ui.components.SimpleSearchBar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -0,0 +1,109 @@
package com.greenart7c3.nostrsigner.ui.components
import android.view.ViewTreeObserver
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleSearchBar(
textFieldState: TextFieldState,
onSearch: (String) -> Unit,
searchResults: List<String>,
modifier: Modifier = Modifier,
) {
// Controls expansion state of the search bar
var expanded by rememberSaveable { mutableStateOf(false) }
var localSearchResults by remember { mutableStateOf(searchResults) }
val view = LocalView.current
val viewTreeObserver = view.viewTreeObserver
DisposableEffect(viewTreeObserver) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
val isKeyboardOpen = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) != false
if (!isKeyboardOpen) {
onSearch(textFieldState.text.toString())
expanded = false
}
}
viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
}
Box(
modifier
.semantics { isTraversalGroup = true },
) {
SearchBar(
windowInsets = WindowInsets(0),
inputField = {
SearchBarDefaults.InputField(
modifier = modifier
.fillMaxWidth(),
query = textFieldState.text.toString(),
onQueryChange = {
textFieldState.edit { replace(0, length, it) }
localSearchResults = searchResults.filter { result ->
result.contains(textFieldState.text.toString(), ignoreCase = true)
}
},
onSearch = {
onSearch(textFieldState.text.toString())
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Search") },
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
// Display search results in a scrollable column
LazyColumn {
items(localSearchResults) { result ->
ListItem(
headlineContent = { Text(result) },
modifier = Modifier
.clickable {
textFieldState.edit { replace(0, length, result) }
onSearch(textFieldState.text.toString())
expanded = false
}
.background(Color.Red)
.fillMaxWidth(),
)
}
}
}
}
}
@@ -178,6 +178,12 @@ sealed class Route(
route = "Feedback",
icon = R.drawable.settings,
)
data object Activities : Route(
title = Amber.instance.getString(R.string.activities),
route = "Activities",
icon = R.drawable.incoming_request,
)
}
val routes = listOf(
@@ -207,4 +213,5 @@ val routes = listOf(
Route.TorSettings,
Route.EditProfile,
Route.Feedback,
Route.Activities,
)
+1
View File
@@ -537,4 +537,5 @@
<string name="create_anonymously">Create anonymously</string>
<string name="send">Send</string>
<string name="feedback_sent">Feedback sent</string>
<string name="activities">Activities</string>
</resources>