Skip to content

Commit

Permalink
Fix padding and add admin control for member location sharing.
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-sneh-s committed Jan 8, 2025
1 parent 9861bdb commit 3897b50
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
Expand All @@ -18,13 +19,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -47,8 +52,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
Expand All @@ -63,7 +70,6 @@ import com.canopas.yourspace.ui.component.AppProgressIndicator
import com.canopas.yourspace.ui.component.NoInternetScreen
import com.canopas.yourspace.ui.component.PrimaryTextButton
import com.canopas.yourspace.ui.component.UserProfile
import com.canopas.yourspace.ui.flow.settings.profile.UserTextField
import com.canopas.yourspace.ui.theme.AppTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -194,21 +200,6 @@ private fun SpaceProfileToolbar() {
}
},
actions = {
Text(
text = stringResource(id = R.string.edit_profile_toolbar_save_text),
color = if (state.allowSave) AppTheme.colorScheme.primary else AppTheme.colorScheme.textDisabled,
style = AppTheme.appTypography.button,
modifier = Modifier
.padding(end = 8.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
enabled = state.allowSave,
onClick = {
viewModel.saveSpace()
}
)
)
if (state.isAdmin && state.spaceMemberCount > 1) {
IconButton(
onClick = { viewModel.onAdminMenuExpanded(true) }
Expand Down Expand Up @@ -243,77 +234,129 @@ private fun SpaceProfileContent() {
val scrollState = rememberScrollState()
val context = LocalContext.current

val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val outlineColor =
if (isFocused) AppTheme.colorScheme.primary else AppTheme.colorScheme.outline

Box(modifier = Modifier.fillMaxSize()) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(bottom = 80.dp)
) {
UserTextField(
label = stringResource(R.string.space_setting_hint_space_name),
text = state.spaceName ?: "",
enabled = state.isAdmin,
onValueChange = {
viewModel.onNameChanged(it.trimStart())
Text(
text = stringResource(id = R.string.space_setting_hint_space_name),
color = if (isFocused) AppTheme.colorScheme.primary else AppTheme.colorScheme.textDisabled,
style = AppTheme.appTypography.caption,
modifier = Modifier.padding(start = 16.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
BasicTextField(
value = state.spaceName ?: "",
onValueChange = { viewModel.onNameChanged(it.trimStart()) },
enabled = state.isAdmin,
maxLines = 1,
interactionSource = interactionSource,
modifier = Modifier
.weight(1f)
.padding(top = 8.dp),
singleLine = true,
textStyle = AppTheme.appTypography.subTitle2.copy(color = AppTheme.colorScheme.textPrimary),
keyboardActions = KeyboardActions(onDone = {
focusManager.clearFocus()
}),
cursorBrush = SolidColor(AppTheme.colorScheme.primary)
)
if (state.allowSave) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "",
tint = outlineColor,
modifier = Modifier
.padding(horizontal = 8.dp)
.clickable {
viewModel.saveSpace()
focusManager.clearFocus()
}
)
}
}

HorizontalDivider(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
color = outlineColor
)

Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(12.dp))

Text(
text = stringResource(R.string.space_invite_code_title),
style = AppTheme.appTypography.body2,
color = AppTheme.colorScheme.textDisabled,
modifier = Modifier.padding(start = 8.dp)
modifier = Modifier.padding(start = 16.dp)
)

Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp)
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = state.inviteCode,
modifier = Modifier.weight(1f),
style = AppTheme.appTypography.header4
)

if (state.isAdmin) {
if (state.isCodeLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
IconButton(onClick = { viewModel.regenerateInviteCode() }) {
Icon(Icons.Default.Refresh, contentDescription = "")
}
}
}

IconButton(
onClick = { shareInvitationCode(context = context, code = state.inviteCode) }
) {
Icon(Icons.Default.Share, contentDescription = "")
}
if (state.isAdmin) {
IconButton(onClick = { viewModel.regenerateInviteCode() }) {
Icon(Icons.Default.Refresh, contentDescription = "")
}
}
}
Text(
text = stringResource(R.string.space_invite_code_expire_text, state.codeExpireTime),
style = AppTheme.appTypography.body2,
color = AppTheme.colorScheme.textDisabled,
modifier = Modifier.padding(start = 8.dp)
modifier = Modifier.padding(start = 16.dp)
)

HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
.padding(vertical = 12.dp, horizontal = 16.dp),
color = AppTheme.colorScheme.outline
)

Header(title = stringResource(id = R.string.space_setting_title_your_location))

state.spaceInfo?.members?.firstOrNull { it.user.id == state.currentUserId }?.let {
state.spaceInfo?.members?.firstOrNull { it.user.id == state.currentUserId }?.let { user ->
UserItem(
userInfo = it,
userInfo = user,
isChecked = state.locationEnabled,
enable = true,
isAdmin = state.isAdmin,
currentUser = state.currentUserId!!,
isAdminUser = state.spaceInfo?.space?.admin_id == it.user.id,
onCheckedChange = {
viewModel.onLocationEnabledChanged(it)
isAdminUser = state.spaceInfo?.space?.admin_id == user.user.id,
onCheckedChange = { isChecked ->
viewModel.onLocationEnabledChanged(isChecked)
},
onMemberRemove = {
viewModel.showRemoveMemberConfirmationWithId(true, "")
Expand All @@ -324,7 +367,7 @@ private fun SpaceProfileContent() {
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
.padding(vertical = 16.dp),
color = AppTheme.colorScheme.outline
)

Expand All @@ -335,18 +378,19 @@ private fun SpaceProfileContent() {
?: emptyList()

if (others.isNotEmpty()) {
others.forEach {
others.forEach { user ->
UserItem(
userInfo = it,
isChecked = it.isLocationEnable,
enable = false,
userInfo = user,
isChecked = user.isLocationEnable,
enable = state.isAdmin,
isAdmin = state.isAdmin,
currentUser = state.currentUserId!!,
isAdminUser = state.spaceInfo?.space?.admin_id == it.user.id,
onCheckedChange = {
isAdminUser = state.spaceInfo?.space?.admin_id == user.user.id,
onCheckedChange = { isChecked ->
viewModel.updateMemberLocation(user.user.id, isChecked)
},
onMemberRemove = {
viewModel.showRemoveMemberConfirmationWithId(true, it.user.id)
viewModel.showRemoveMemberConfirmationWithId(true, user.user.id)
}
)
}
Expand Down Expand Up @@ -502,8 +546,8 @@ private fun UserItem(
uncheckedTrackColor = AppTheme.colorScheme.containerHigh,
disabledCheckedTrackColor = AppTheme.colorScheme.containerHigh
),
onCheckedChange = {
onCheckedChange(it)
onCheckedChange = { isChecked ->
onCheckedChange(isChecked)
},
modifier = Modifier.padding(end = 8.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,34 @@ class SpaceProfileViewModel @Inject constructor(
private fun onChange() {
val spaceName = _state.value.spaceInfo?.space?.name
val validFirstName = (_state.value.spaceName ?: "").trim().length >= 3

val locationEnabled =
_state.value.spaceInfo?.members?.firstOrNull { it.user.id == authService.currentUser?.id }?.isLocationEnable
?: false

val changes =
spaceName != _state.value.spaceName || locationEnabled != _state.value.locationEnabled
val changes = spaceName != _state.value.spaceName

_state.value = state.value.copy(allowSave = validFirstName && changes)
}

fun onLocationEnabledChanged(enable: Boolean) {
_state.value = state.value.copy(locationEnabled = enable)
onChange()
viewModelScope.launch {
_state.value = state.value.copy(locationEnabled = enable)
spaceRepository.enableLocation(spaceID, authService.currentUser?.id ?: "", enable)
onChange()
}
}

fun updateMemberLocation(memberId: String, enableLocation: Boolean) {
viewModelScope.launch(appDispatcher.IO) {
try {
spaceRepository.enableLocation(spaceID, memberId, enableLocation)
val spaceInfo = spaceRepository.getSpaceInfo(spaceID)
_state.emit(
_state.value.copy(
spaceInfo = spaceInfo,
locationEnabledChanges = mapOf(memberId to enableLocation)
)
)
} catch (e: Exception) {
Timber.e(e, "Failed to update member location")
}
}
}

private fun fetchInviteCode(spaceId: String) {
Expand All @@ -112,10 +126,10 @@ class SpaceProfileViewModel @Inject constructor(

fun regenerateInviteCode() = viewModelScope.launch(appDispatcher.IO) {
if (state.value.isAdmin) {
_state.emit(_state.value.copy(isLoading = true))
_state.emit(_state.value.copy(isCodeLoading = true))
spaceRepository.regenerateInviteCode(spaceRepository.currentSpaceId)
_state.emit(_state.value.copy(isLoading = false))
fetchSpaceDetail()
fetchInviteCode(spaceID)
_state.emit(_state.value.copy(isCodeLoading = false))
}
}

Expand Down Expand Up @@ -166,11 +180,11 @@ class SpaceProfileViewModel @Inject constructor(
_state.value.locationEnabled
)
}
_state.emit(_state.value.copy(saving = false))
navigator.navigateBack()
val spaceInfo = spaceRepository.getSpaceInfo(spaceID)
_state.emit(_state.value.copy(saving = false, allowSave = false, spaceInfo = spaceInfo))
} catch (e: Exception) {
Timber.e(e, "Failed to save space")
_state.emit(_state.value.copy(saving = false, error = e))
_state.emit(_state.value.copy(saving = false, error = e, allowSave = false))
}
}

Expand Down Expand Up @@ -310,5 +324,9 @@ data class SpaceProfileState(
val showChangeAdminDialog: Boolean = false,
var isMenuExpanded: Boolean = false,
val inviteCode: String = "",
val codeExpireTime: String = ""
val codeExpireTime: String = "",
val isCodeLoading: Boolean = false,
val locationEnabledChanges: Map<String, Boolean> = emptyMap(),
val isLocationSettingChange: Boolean = false,
val userLocationUpdatingId: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ class SpaceProfileViewModelTest {
setup()
viewModel.fetchSpaceDetail()
viewModel.onLocationEnabledChanged(false)
assert(viewModel.state.value.allowSave)
}

@Test
Expand Down Expand Up @@ -251,7 +250,6 @@ class SpaceProfileViewModelTest {
setup()
viewModel.fetchSpaceDetail()
viewModel.onLocationEnabledChanged(false)
viewModel.saveSpace()
verify(spaceRepository).enableLocation(space.id, user1.id, false)
}

Expand All @@ -273,7 +271,6 @@ class SpaceProfileViewModelTest {
setup()
viewModel.fetchSpaceDetail()
viewModel.saveSpace()
verify(navigator).navigateBack()
}

@Test
Expand Down

0 comments on commit 3897b50

Please sign in to comment.