Skip to content

Commit

Permalink
Add filter option for location history
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-radhika-s committed Feb 1, 2024
1 parent aa550b3 commit 3cd70bf
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ fun MapScreen() {
LaunchedEffect(bottomSheetState) {
snapshotFlow { bottomSheetState.currentValue }
.collect {
Timber.e("XXX sheet state ${it.toString()}")
if (it == SheetValue.Hidden) {
viewModel.dismissMemberDetail()
}
Expand All @@ -85,7 +84,6 @@ fun MapScreen() {
sheetContainerColor = AppTheme.colorScheme.surface,
sheetPeekHeight = if(state.showUserDetails) (screenHeight / 3).dp else 0.dp,
sheetContent = {
Timber.e("XXX selected ${state.selectedUser}")
state.selectedUser?.let { MemberDetailBottomSheetContent(state.selectedUser!!) }
}
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package com.canopas.catchme.ui.flow.home.map.member

import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
Expand All @@ -20,13 +21,14 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Place
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
Expand All @@ -35,11 +37,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
Expand All @@ -56,6 +60,7 @@ import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.abs

Expand All @@ -71,7 +76,11 @@ fun MemberDetailBottomSheetContent(
val calendar = Calendar.getInstance()
calendar.add(Calendar.HOUR_OF_DAY, -24)
val timestamp = calendar.timeInMillis
viewModel.fetchUserLocationHistory(userInfo, timestamp)
viewModel.fetchUserLocationHistory(
userInfo,
from = timestamp,
to = System.currentTimeMillis()
)
}

Column(modifier = Modifier.fillMaxHeight(0.9f)) {
Expand All @@ -80,25 +89,67 @@ fun MemberDetailBottomSheetContent(
Divider(thickness = 1.dp, color = AppTheme.colorScheme.outline)
Spacer(modifier = Modifier.height(10.dp))

FilterOption {}
FilterOption(
selectedFromTimestamp = state.selectedTimeFrom ?: 0,
selectedToTimestamp = state.selectedTimeTo ?: 0
) {
val calendar = Calendar.getInstance()
calendar.timeInMillis = it
calendar.add(Calendar.DAY_OF_MONTH, 1)
viewModel.fetchLocationHistory(from = it, to = calendar.timeInMillis)
}

LocationHistory(state.location)
LocationHistory(state.location, state.isLoading)

}

}

@Composable
fun LocationHistory(
locations: List<ApiLocation>
locations: List<ApiLocation>,
isLoading: Boolean
) {
LazyColumn(contentPadding = PaddingValues(bottom = 30.dp)) {
itemsIndexed(locations) { index, location ->
LocationHistoryItem(location, index, isLastItem = index == locations.lastIndex)
Box {
if (locations.isEmpty() && !isLoading) {
EmptyHistory()
} else {
LazyColumn(contentPadding = PaddingValues(bottom = 30.dp)) {
itemsIndexed(locations) { index, location ->
LocationHistoryItem(location, index, isLastItem = index == locations.lastIndex)
}
}
}
}
}

@Composable
private fun EmptyHistory() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_empty_location_history),
contentDescription = "",
modifier = Modifier
.padding(bottom = 30.dp)
.alpha(0.8f),
)

Text(
text = stringResource(id = R.string.member_detail_empty_location_history),
style = AppTheme.appTypography.body1.copy(
color = AppTheme.colorScheme.containerHigh.copy(
alpha = 0.8f
)
),
modifier = Modifier.padding(bottom = 30.dp)
)
}
}

@Composable
private fun LocationHistoryItem(location: ApiLocation, index: Int, isLastItem: Boolean) {
val context = LocalContext.current
Expand Down Expand Up @@ -186,7 +237,14 @@ private fun LocationHistoryItem(location: ApiLocation, index: Int, isLastItem: B
}

@Composable
fun FilterOption(onClick: () -> Unit = {}) {
fun FilterOption(
selectedFromTimestamp: Long,
selectedToTimestamp: Long,
onTimeSelected: (Long) -> Unit = {}
) {
var showDatePicker by remember {
mutableStateOf(false)
}
Row(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -200,10 +258,12 @@ fun FilterOption(onClick: () -> Unit = {}) {
style = AppTheme.appTypography.body2.copy(color = AppTheme.colorScheme.textSecondary)
)
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onClick) {
TextButton(onClick = {
showDatePicker = true
}) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Today",
text = getFormattedFilterLabel(selectedFromTimestamp, selectedToTimestamp),
style = AppTheme.appTypography.body2.copy(color = AppTheme.colorScheme.textSecondary),
modifier = Modifier.padding(end = 5.dp)
)
Expand All @@ -219,6 +279,51 @@ fun FilterOption(onClick: () -> Unit = {}) {

}

if (showDatePicker) {
ShowDatePicker(selectedFromTimestamp,
confirmButtonClick = { timestamp ->
showDatePicker = false

onTimeSelected(timestamp)

}, dismissButtonClick = {
showDatePicker = false
})
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShowDatePicker(
selectedTimestamp: Long? = null,
confirmButtonClick: (Long) -> Unit,
dismissButtonClick: () -> Unit
) {
val calendar = Calendar.getInstance()
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = selectedTimestamp ?: calendar.timeInMillis
)
DatePickerDialog(onDismissRequest = {},
confirmButton = {
TextButton(onClick = {
confirmButtonClick(
datePickerState.selectedDateMillis ?: calendar.timeInMillis
)
}) {
Text(text = "Confirm")
}
},
dismissButton = {
TextButton(onClick = dismissButtonClick) {
Text(text = "Cancel")
}
}) {
DatePicker(
state = datePickerState,
dateValidator = { date -> date <= System.currentTimeMillis() }
)
}

}

@Composable
Expand All @@ -230,14 +335,28 @@ fun UserInfoContent(userInfo: UserInfo) {
.padding(horizontal = 16.dp)
) {
UserProfile(modifier = Modifier.size(54.dp), user = userInfo.user)
Text(
text = userInfo.user.fullName,

Column(
modifier = Modifier
.padding(start = 16.dp)
.weight(1f),
style = AppTheme.appTypography.header3,
maxLines = 1,
)
.weight(1f), verticalArrangement = Arrangement.Center
) {
Text(
text = userInfo.user.fullName,
style = AppTheme.appTypography.header3,
maxLines = 1,
)
if (!userInfo.isLocationEnable)
Text(
text = stringResource(id = R.string.map_user_item_location_off),
style = AppTheme.appTypography.label1.copy(
color = Color.Red,
fontWeight = FontWeight.Normal
)
)

}


Box(
modifier = Modifier
Expand All @@ -264,22 +383,30 @@ fun UserInfoContent(userInfo: UserInfo) {

private fun getFormattedTimeString(context: Context, timestamp: Long): String {
val now = System.currentTimeMillis()
val duration = abs(timestamp - now)
val days = TimeUnit.MILLISECONDS.toDays(duration)
val hours = TimeUnit.MILLISECONDS.toHours(duration)
val minutes = TimeUnit.MILLISECONDS.toMinutes(duration)

val elapsedTime = now - timestamp
return when {
minutes < 1 -> context.getString(R.string.map_user_item_location_updated_now)
hours < 1 -> context.getString(
elapsedTime < TimeUnit.MINUTES.toMillis(1) -> context.getString(R.string.map_user_item_location_updated_now)
elapsedTime < TimeUnit.HOURS.toMillis(1) -> context.getString(
R.string.map_user_item_location_updated_minutes_ago,
minutes.toString()
"${TimeUnit.MILLISECONDS.toMinutes(elapsedTime)}"
)

else -> {
val output = SimpleDateFormat("h:mm a")
output.format(Date(timestamp))
}
elapsedTime < TimeUnit.DAYS.toMillis(1) ->
SimpleDateFormat("h:mm a", Locale.getDefault()).format(Date(timestamp))

else -> SimpleDateFormat("h:mm a • d MMM", Locale.getDefault()).format(Date(timestamp))
}
}

fun getFormattedFilterLabel(startTimestamp: Long, endTimestamp: Long): String {
val startDate = Date(startTimestamp)
val endDate = Date(endTimestamp)

val startDateFormat = SimpleDateFormat("d MMM", Locale.getDefault())
val endDateFormat = SimpleDateFormat("d MMM", Locale.getDefault())

val startDateFormatted = startDateFormat.format(startDate)
val endDateFormatted = endDateFormat.format(endDate)

return "$startDateFormatted$endDateFormatted"
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package com.canopas.catchme.ui.flow.home.map.member

import android.location.Location
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.canopas.catchme.data.models.location.ApiLocation
import com.canopas.catchme.data.models.user.UserInfo
import com.canopas.catchme.data.service.location.ApiLocationService
import com.canopas.catchme.data.utils.AppDispatcher
import com.canopas.catchme.ui.flow.home.map.distanceTo
import com.google.android.gms.maps.model.LatLng
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -26,19 +23,20 @@ class MemberDetailViewModel @Inject constructor(
private val _state = MutableStateFlow(MemberDetailState())
var state = _state.asStateFlow()

fun fetchUserLocationHistory(userInfo: UserInfo, timestamp: Long) {
_state.value = _state.value.copy(selectedUser = userInfo, selectedTime = timestamp)
fetchLocationHistory(timestamp)
fun fetchUserLocationHistory(userInfo: UserInfo, from: Long, to: Long) {
_state.value =
_state.value.copy(selectedUser = userInfo, selectedTimeFrom = from, selectedTimeTo = to)
fetchLocationHistory(from, to)
}

fun fetchLocationHistory(timestamp: Long) = viewModelScope.launch(appDispatcher.IO) {
_state.emit(_state.value.copy(selectedTime = timestamp))
fun fetchLocationHistory(from: Long, to: Long) = viewModelScope.launch(appDispatcher.IO) {
_state.emit(_state.value.copy(selectedTimeFrom = from, selectedTimeTo = to))

try {
_state.emit(_state.value.copy(isLoading = true))
locationService.getLocationHistory(
_state.value.selectedUser?.user?.id ?: "",
timestamp
from, to
).collectLatest {
val locations = it
.distinctBy { it.latitude }
Expand All @@ -56,7 +54,8 @@ class MemberDetailViewModel @Inject constructor(

data class MemberDetailState(
val selectedUser: UserInfo? = null,
val selectedTime: Long? = null,
val selectedTimeFrom: Long? = null,
val selectedTimeTo: Long? = null,
val location: List<ApiLocation> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<string name="map_user_item_location_unknown">Location not found</string>
<string name="map_user_item_location_off">Location turned off</string>
<string name="member_detail_location_history">Location History</string>
<string name="member_detail_empty_location_history">No Location History Found!</string>

<string-array name="home_create_space_name_suggestion">
<item>Family</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ class ApiLocationService @Inject constructor(
.orderBy("created_at", Query.Direction.DESCENDING).limit(1)
.snapshotFlow(ApiLocation::class.java)

suspend fun getLocationHistory(userId: String, timestamp: Long) =
suspend fun getLocationHistory(userId: String, from: Long, to:Long) =
locationRef.whereEqualTo("user_id", userId)
.whereGreaterThanOrEqualTo("created_at", timestamp)
.whereGreaterThanOrEqualTo("created_at", from)
.whereLessThan("created_at", to)
.orderBy("created_at", Query.Direction.DESCENDING)
.snapshotFlow(ApiLocation::class.java)

Expand Down

0 comments on commit 3cd70bf

Please sign in to comment.