Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android: move node search to background and fix avatar padding #574

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns)
initViewModels()
applicationScope.launch {
Notifier.state.collect { state ->
Notifier.state.collect { _ ->
combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled ->
Pair(state, forceEnabled)
}
Expand Down
18 changes: 18 additions & 0 deletions android/src/main/java/com/tailscale/ipn/ui/util/ModifierUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package com.tailscale.ipn.ui.util

import androidx.compose.ui.Modifier

/// Applies different modifiers to the receiver based on a condition.
inline fun Modifier.conditional(
condition: Boolean,
ifTrue: Modifier.() -> Modifier,
ifFalse: Modifier.() -> Modifier = { this },
): Modifier =
if (condition) {
then(ifTrue(Modifier))
} else {
then(ifFalse(Modifier))
}
86 changes: 42 additions & 44 deletions android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.util.AndroidTVUtil
import com.tailscale.ipn.ui.util.conditional

@OptIn(ExperimentalCoilApi::class)
@Composable
Expand All @@ -43,53 +45,49 @@ fun Avatar(
var isFocused = remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current

// Outer Box for the larger focusable and clickable area
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(4.dp)
.size((size * 1.5f).dp) // Focusable area is larger than the avatar
.clip(CircleShape) // Ensure both the focus and click area are circular
.background(
if (isFocused.value) MaterialTheme.colorScheme.surface
else Color.Transparent,
)
.onFocusChanged { focusState ->
isFocused.value = focusState.isFocused
}
.focusable() // Make this outer Box focusable (after onFocusChanged)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = true), // Apply ripple effect inside circular bounds
onClick = {
action?.invoke()
// Outer Box for the larger focusable and clickable area
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier.conditional(AndroidTVUtil.isAndroidTV(), { padding(4.dp) })
.conditional(
AndroidTVUtil.isAndroidTV(),
{
size((size * 1.5f).dp) // Focusable area is larger than the avatar
})
.clip(CircleShape) // Ensure both the focus and click area are circular
.background(
if (isFocused.value) MaterialTheme.colorScheme.surface else Color.Transparent,
)
.onFocusChanged { focusState -> isFocused.value = focusState.isFocused }
.focusable() // Make this outer Box focusable (after onFocusChanged)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = true), // Apply ripple effect inside circular bounds
onClick = {
action?.invoke()
focusManager.clearFocus() // Clear focus after clicking the avatar
}
)
) {
})) {
// Inner Box to hold the avatar content (Icon or AsyncImage)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(size.dp)
.clip(CircleShape)
) {
// Always display the default icon as a background layer
Icon(
imageVector = Icons.Default.Person,
contentDescription = stringResource(R.string.settings_title),
modifier =
Modifier.size((size * 0.8f).dp)
.clip(CircleShape) // Icon size slightly smaller than the Box
)
modifier = Modifier.size(size.dp).clip(CircleShape)) {
// Always display the default icon as a background layer
Icon(
imageVector = Icons.Default.Person,
contentDescription = stringResource(R.string.settings_title),
modifier =
Modifier.conditional(AndroidTVUtil.isAndroidTV(), { size((size * 0.8f).dp) })
.clip(CircleShape) // Icon size slightly smaller than the Box
)

// Overlay the profile picture if available
profile?.UserProfile?.ProfilePicURL?.let { url ->
AsyncImage(
model = url,
modifier = Modifier.size(size.dp).clip(CircleShape),
contentDescription = null)
}
}
}
// Overlay the profile picture if available
profile?.UserProfile?.ProfilePicURL?.let { url ->
AsyncImage(
model = url,
modifier = Modifier.size(size.dp).clip(CircleShape),
contentDescription = null)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import com.tailscale.ipn.ui.util.PeerCategorizer
import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.TimeUtil
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
Expand Down Expand Up @@ -77,6 +79,8 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {

val isVpnActive: StateFlow<Boolean> = vpnViewModel.vpnActive

var searchJob: Job? = null

// Icon displayed in the button to present the health view
val healthIcon: StateFlow<Int?> = MutableStateFlow(null)

Expand Down Expand Up @@ -130,18 +134,25 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {

viewModelScope.launch {
_searchTerm.debounce(250L).collect { term ->
val filteredPeers = peerCategorizer.groupedAndFilteredPeers(term)
_peers.value = filteredPeers
// run the search as a background task
searchJob?.cancel()
searchJob =
launch(Dispatchers.Default) {
val filteredPeers = peerCategorizer.groupedAndFilteredPeers(term)
_peers.value = filteredPeers
}
}
}

viewModelScope.launch {
Notifier.netmap.collect { it ->
it?.let { netmap ->
peerCategorizer.regenerateGroupedPeers(netmap)

// Immediately update _peers with the full peer list
_peers.value = peerCategorizer.groupedAndFilteredPeers(searchTerm.value)
searchJob?.cancel()
launch(Dispatchers.Default) {
peerCategorizer.regenerateGroupedPeers(netmap)
val filteredPeers = peerCategorizer.groupedAndFilteredPeers(searchTerm.value)
_peers.value = filteredPeers
}

if (netmap.SelfNode.keyDoesNotExpire) {
showExpiry.set(false)
Expand Down