mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Add unread count badge to bottom nav (#3440)
This commit is contained in:
parent
91470667fb
commit
cd1a54f506
8 changed files with 67 additions and 54 deletions
|
|
@ -52,6 +52,7 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
|||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.QuickChatActionRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
|
|
@ -134,6 +135,7 @@ constructor(
|
|||
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
||||
private val meshServiceNotifications: MeshServiceNotifications,
|
||||
private val analytics: PlatformAnalytics,
|
||||
packetRepository: PacketRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
|
||||
|
|
@ -209,6 +211,12 @@ constructor(
|
|||
val channels: StateFlow<AppOnlyProtos.ChannelSet>
|
||||
get() = _channels
|
||||
|
||||
val unreadMessageCount =
|
||||
packetRepository
|
||||
.getUnreadCountTotal()
|
||||
.map { it.coerceAtLeast(0) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), 0)
|
||||
|
||||
val quickChatActions
|
||||
get() =
|
||||
quickChatActionRepository
|
||||
|
|
|
|||
|
|
@ -22,9 +22,14 @@ package com.geeksville.mesh.ui
|
|||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -35,7 +40,11 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Wifi
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -52,6 +61,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -85,7 +95,7 @@ import com.geeksville.mesh.repository.radio.MeshActivity
|
|||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
|
||||
import com.geeksville.mesh.ui.connections.DeviceType
|
||||
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
|
||||
import com.geeksville.mesh.ui.connections.components.ConnectionsNavIcon
|
||||
import com.geeksville.mesh.ui.metrics.annotateTraceroute
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -137,6 +147,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val unreadMessageCount by uIViewModel.unreadMessageCount.collectAsStateWithLifecycle()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
|
@ -279,8 +290,9 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
val iconModifier =
|
||||
if (isConnectionsRoute) {
|
||||
if (isConnectionsRoute) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.drawWithCache {
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
|
|
@ -307,12 +319,38 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
) {
|
||||
ConnectionsNavIcon(
|
||||
connectionState = connectionState,
|
||||
deviceType = DeviceType.fromAddress(selectedDevice),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (destination == TopLevelDestination.Conversations) {
|
||||
// Keep track of the last non-zero count for display during exit animation
|
||||
var lastNonZeroCount by remember { mutableIntStateOf(unreadMessageCount) }
|
||||
if (unreadMessageCount > 0) {
|
||||
lastNonZeroCount = unreadMessageCount
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = unreadMessageCount > 0,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut(),
|
||||
) {
|
||||
Badge { Text(lastNonZeroCount.toString()) }
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = destination.icon,
|
||||
contentDescription = stringResource(id = destination.label),
|
||||
tint = LocalContentColor.current,
|
||||
)
|
||||
}
|
||||
Box(modifier = iconModifier) {
|
||||
TopLevelNavIcon(destination, connectionState, DeviceType.fromAddress(selectedDevice))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.compose.material.icons.rounded.Snooze
|
|||
import androidx.compose.material.icons.rounded.Usb
|
||||
import androidx.compose.material.icons.rounded.Wifi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -31,12 +30,10 @@ import androidx.compose.ui.draw.drawWithContent
|
|||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.TopLevelDestination
|
||||
import com.geeksville.mesh.ui.connections.DeviceType
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.icon.Device
|
||||
|
|
@ -48,24 +45,7 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
|
||||
@Composable
|
||||
fun TopLevelNavIcon(destination: TopLevelDestination, connectionState: ConnectionState, deviceType: DeviceType?) {
|
||||
if (destination == TopLevelDestination.Connections) {
|
||||
ConnectionsNavIcon(connectionState = connectionState, deviceType = deviceType)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = destination.icon,
|
||||
contentDescription = stringResource(id = destination.label),
|
||||
tint = LocalContentColor.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectionsNavIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
connectionState: ConnectionState,
|
||||
deviceType: DeviceType?,
|
||||
) {
|
||||
fun ConnectionsNavIcon(modifier: Modifier = Modifier, connectionState: ConnectionState, deviceType: DeviceType?) {
|
||||
val tint =
|
||||
when (connectionState) {
|
||||
ConnectionState.DISCONNECTED -> colorScheme.StatusRed
|
||||
|
|
@ -105,10 +85,6 @@ private fun ConnectionsNavIcon(
|
|||
)
|
||||
}
|
||||
|
||||
class TopLevelDestinationProvider : PreviewParameterProvider<TopLevelDestination> {
|
||||
override val values: Sequence<TopLevelDestination> = TopLevelDestination.entries.asSequence()
|
||||
}
|
||||
|
||||
class ConnectionStateProvider : PreviewParameterProvider<ConnectionState> {
|
||||
override val values: Sequence<ConnectionState> =
|
||||
sequenceOf(ConnectionState.CONNECTED, ConnectionState.DEVICE_SLEEP, ConnectionState.DISCONNECTED)
|
||||
|
|
@ -118,20 +94,6 @@ class DeviceTypeProvider : PreviewParameterProvider<DeviceType> {
|
|||
override val values: Sequence<DeviceType> = sequenceOf(DeviceType.BLE, DeviceType.TCP, DeviceType.USB)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TopLevelNavIconPreviewConnectionStates(
|
||||
@PreviewParameter(TopLevelDestinationProvider::class) destination: TopLevelDestination,
|
||||
) {
|
||||
AppTheme {
|
||||
TopLevelNavIcon(
|
||||
destination = destination,
|
||||
connectionState = ConnectionState.CONNECTED,
|
||||
deviceType = DeviceType.BLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ConnectionsNavIconPreviewConnectionStates(
|
||||
Loading…
Add table
Add a link
Reference in a new issue