mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-16 11:57:35 -04:00
Add unread count badge to bottom nav (#3440)
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<ID>CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */</ID>
|
||||
<ID>ComposableParamOrder:Channel.kt$ChannelScreen</ID>
|
||||
<ID>ComposableParamOrder:Channel.kt$EditChannelUrl</ID>
|
||||
<ID>ComposableParamOrder:ConnectionsNavIcon.kt$ConnectionsNavIcon</ID>
|
||||
<ID>ComposableParamOrder:DeviceMetrics.kt$DeviceMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:EmptyStateContent.kt$EmptyStateContent</ID>
|
||||
<ID>ComposableParamOrder:EnvironmentCharts.kt$ChartContent</ID>
|
||||
@@ -27,7 +28,6 @@
|
||||
<ID>ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter</ID>
|
||||
<ID>ComposableParamOrder:Share.kt$ShareScreen</ID>
|
||||
<ID>ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:TopLevelNavIcon.kt$ConnectionsNavIcon</ID>
|
||||
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
|
||||
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
|
||||
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
|
||||
@@ -135,7 +135,6 @@
|
||||
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
|
||||
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
|
||||
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
|
||||
<ID>ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon</ID>
|
||||
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier.width(dp)</ID>
|
||||
|
||||
@@ -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(
|
||||
@@ -44,6 +44,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: Lazy<Packe
|
||||
|
||||
suspend fun getUnreadCount(contact: String): Int = withContext(Dispatchers.IO) { packetDao.getUnreadCount(contact) }
|
||||
|
||||
fun getUnreadCountTotal(): Flow<Int> = packetDao.getUnreadCountTotal()
|
||||
|
||||
suspend fun clearUnreadCount(contact: String, timestamp: Long) =
|
||||
withContext(Dispatchers.IO) { packetDao.clearUnreadCount(contact, timestamp) }
|
||||
|
||||
|
||||
@@ -79,6 +79,15 @@ interface PacketDao {
|
||||
)
|
||||
suspend fun getUnreadCount(contact: String): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND read = 0
|
||||
""",
|
||||
)
|
||||
fun getUnreadCountTotal(): Flow<Int>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE packet
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<ID>ComposableParamOrder:SwitchPreference.kt$SwitchPreference</ID>
|
||||
<ID>ContentSlotReused:AdaptiveTwoPane.kt$second</ID>
|
||||
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onClickChip</ID>
|
||||
<ID>LongMethod:EditTextPreference.kt$@Composable fun EditTextPreference( title: String, value: String, enabled: Boolean, isError: Boolean, keyboardOptions: KeyboardOptions, keyboardActions: KeyboardActions, onValueChanged: (String) -> Unit, modifier: Modifier = Modifier, summary: String? = null, maxSize: Int = 0, // max_size - 1 (in bytes) onFocusChanged: (FocusState) -> Unit = {}, trailingIcon: (@Composable () -> Unit)? = null, visualTransformation: VisualTransformation = VisualTransformation.None, )</ID>
|
||||
<ID>MagicNumber:EditIPv4Preference.kt$0xff</ID>
|
||||
<ID>MagicNumber:EditIPv4Preference.kt$16</ID>
|
||||
<ID>MagicNumber:EditIPv4Preference.kt$24</ID>
|
||||
@@ -60,6 +59,5 @@
|
||||
<ID>PreviewPublic:SignalInfo.kt$SignalInfoPreview</ID>
|
||||
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSelfPreview</ID>
|
||||
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview</ID>
|
||||
<ID>UnusedParameter:DropDownPreference.kt$modifier: Modifier = Modifier</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<ID>CyclomaticComplexMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
|
||||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>LambdaParameterEventTrailing:NodeActionButton.kt$onClick</ID>
|
||||
<ID>LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
|
||||
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
|
||||
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
|
||||
<ID>LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
|
||||
@@ -34,10 +33,8 @@
|
||||
<ID>MagicNumber:EditChannelDialog.kt$16</ID>
|
||||
<ID>MagicNumber:EditChannelDialog.kt$32</ID>
|
||||
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
|
||||
<ID>MagicNumber:PowerConfigItemList.kt$3600</ID>
|
||||
<ID>ModifierMissing:ChannelSettingsItemList.kt$ChannelSelection</ID>
|
||||
<ID>ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen</ID>
|
||||
<ID>ModifierMissing:LoRaConfigItemList.kt$LoRaConfigScreen</ID>
|
||||
<ID>ModifierMissing:MapReportingPreference.kt$MapReportingPreference</ID>
|
||||
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen</ID>
|
||||
<ID>ModifierMissing:PositionConfigItemList.kt$PositionConfigScreen</ID>
|
||||
|
||||
Reference in New Issue
Block a user