Files
2026-08-03 11:51:02 +03:00

311 lines
11 KiB
Kotlin

package com.tutpro.baresip
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.tutpro.baresip.CustomElements.AlertDialog
import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.util.GregorianCalendar
fun NavGraphBuilder.blockedScreenRoute(navController: NavController) {
composable(
route = "blocked/{request}/{aor}",
arguments = listOf(
navArgument("aor") { type = NavType.StringType },
navArgument("request") { type = NavType.StringType }
)
) { backStackEntry ->
val aor = backStackEntry.arguments?.getString("aor")!!
val request = backStackEntry.arguments?.getString("request")!!
BlockedScreen(navController, request, aor)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BlockedScreen(navController: NavController, request: String, aor: String) {
val account = Account.ofAor(aor)!!
val blocked: MutableState<List<Blocked>> = remember { mutableStateOf(emptyList()) }
var isBlockedLoaded by remember { mutableStateOf(false) }
var refreshTrigger by remember { mutableIntStateOf(0) }
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(aor, refreshTrigger) {
blocked.value = loadBlocked(request, aor)
isBlockedLoaded = true
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) refreshTrigger++
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
BackHandler(enabled = true) { navController.navigateUp() }
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
containerColor = MaterialTheme.colorScheme.background,
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
Spacer(Modifier.statusBarsPadding())
TopAppBar(navController, account, request, blocked)
}
},
content = { contentPadding ->
if (isBlockedLoaded)
BlockedContent(LocalContext.current, navController, contentPadding, account, blocked)
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopAppBar(
navController: NavController,
account: Account,
request: String,
blocked: MutableState<List<Blocked>>
) {
var expanded by remember { mutableStateOf(false) }
val delete = stringResource(R.string.delete)
val showDialog = remember { mutableStateOf(false) }
val lastAction = remember { mutableStateOf({}) }
AlertDialog(
showDialog = showDialog,
title = stringResource(R.string.confirmation),
message = String.format(stringResource(R.string.blocked_delete_alert), account.text()),
firstButtonText = stringResource(R.string.cancel),
lastButtonText = stringResource(R.string.delete),
onLastClicked = lastAction.value,
)
TopAppBar(
title = {
Text(
text = if (request == "invite")
stringResource(R.string.blocked_calls)
else
stringResource(R.string.blocked_messages),
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
),
windowInsets = WindowInsets(0, 0, 0, 0),
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
},
actions = {
IconButton(onClick = { expanded = !expanded }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu")
}
CustomElements.DropdownMenu(
expanded,
{ expanded = false },
listOf(delete),
onItemClick = { selectedItem ->
expanded = false
when (selectedItem) {
delete -> {
lastAction.value = {
Blocked.clear(account.aor)
blocked.value = emptyList()
}
showDialog.value = true
}
}
}
)
}
)
}
@Composable
private fun BlockedContent(
ctx: Context,
navController: NavController,
contentPadding: PaddingValues,
account: Account,
blocked: MutableState<List<Blocked>>
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(contentPadding)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Account(account)
Blocked(ctx, navController, account, blocked)
}
}
@Composable
private fun Account(account: Account) {
Text(
text = stringResource(R.string.account) + " " + account.text(),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun Blocked(
ctx: Context,
navController: NavController,
acc: Account,
blocked: MutableState<List<Blocked>>
) {
val showDialog = remember { mutableStateOf(false) }
val message = remember { mutableStateOf("") }
val lastButtonText = remember { mutableStateOf("") }
val lastAction = remember { mutableStateOf({}) }
val unknown = stringResource(R.string.unknown)
AlertDialog(
showDialog = showDialog,
title = stringResource(R.string.confirmation),
message = message.value,
firstButtonText = stringResource(R.string.cancel),
lastButtonText = lastButtonText.value,
onLastClicked = lastAction.value,
)
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 4.dp)
.verticalScrollbar(state = lazyListState)
.background(MaterialTheme.colorScheme.background),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
items(items = blocked.value, key = { blocked -> blocked.timeStamp }) { blocked ->
val peerUri = blocked.peerUri
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
.clickable(
enabled = !peerUri.contains("anonymous") && peerUri != unknown,
onClick = {
message.value = String.format(
ctx.getString(R.string.blocked_contact_question), peerUri
)
lastButtonText.value = ctx.getString(R.string.add_contact)
lastAction.value = { navController.navigate("contact/$peerUri/new") }
showDialog.value = true
}
)
) {
Text(
text = "\u2022",
modifier = Modifier.padding(start = 8.dp, end = 4.dp),
fontSize = 18.sp
)
Text(
text = Utils.friendlyUri(ctx, peerUri, acc),
fontSize = 18.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.weight(1f))
val calendar = GregorianCalendar()
calendar.timeInMillis = blocked.timeStamp
Text(
text = Utils.relativeTime(ctx, calendar),
fontSize = 12.sp,
minLines = 2, maxLines = 2,
lineHeight = 16.sp,
textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(end = 16.dp)
)
}
}
}
}
private fun loadBlocked(request: String, aor: String): MutableList<Blocked> {
val res = mutableListOf<Blocked>()
for (i in BaresipService.blocked.indices.reversed()) {
val b = BaresipService.blocked[i]
if (b.aor == aor && b.request == request)
res.add(Blocked("", b.peerUri, "", b.timeStamp))
}
Log.d(TAG, "Loaded ${res.size} blocked $request requests")
return res
}