Skip to content

Commit

Permalink
Feature Contact (#126)
Browse files Browse the repository at this point in the history
# *Feature Contact*

## ♻️ Current situation & Problem
* #115 
* The feature was also missing in the Engage-HF app


## ⚙️ Release Notes 
### Contact Module
<img
src="https://github.com/user-attachments/assets/c269291f-bc53-40a6-aa1f-c45efa433610"
width="200" alt="Contact Screen"/>
<img
src="https://github.com/user-attachments/assets/21495328-5c9b-41e1-a079-a1be3b83da6a"
width="200" alt="Contact Screen"/>

### Engage-HF
<img
src="https://github.com/user-attachments/assets/eb05d2a5-bcd7-4ed3-a23b-6c625bc3db8a"
width="200" alt="Contact Screen"/>

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Signed-off-by: Basler182 <[email protected]>
  • Loading branch information
Basler182 authored Oct 21, 2024
1 parent 9eae0b5 commit 0148cf3
Show file tree
Hide file tree
Showing 24 changed files with 713 additions and 52 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
implementation(project(":core:navigation"))
implementation(project(":core:notification"))
implementation(project(":modules:account"))
implementation(project(":modules:contact"))
implementation(project(":modules:education"))
implementation(project(":modules:healthconnectonfhir"))
implementation(project(":modules:onboarding"))
Expand Down
12 changes: 9 additions & 3 deletions app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.navigation.toRoute
import dagger.hilt.android.AndroidEntryPoint
import edu.stanford.bdh.engagehf.bluetooth.BluetoothViewModel
import edu.stanford.bdh.engagehf.bluetooth.data.models.Action
import edu.stanford.bdh.engagehf.contact.ui.ContactScreen
import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent
import edu.stanford.bdh.engagehf.navigation.RegisterParams
import edu.stanford.bdh.engagehf.navigation.Routes
Expand All @@ -35,7 +36,6 @@ import edu.stanford.bdh.engagehf.questionnaire.QuestionnaireScreen
import edu.stanford.spezi.core.coroutines.di.Dispatching
import edu.stanford.spezi.core.design.theme.Sizes
import edu.stanford.spezi.core.design.theme.SpeziTheme
import edu.stanford.spezi.core.logging.speziLogger
import edu.stanford.spezi.core.navigation.NavigationEvent
import edu.stanford.spezi.core.notification.NotificationNavigationEvent
import edu.stanford.spezi.core.notification.NotificationRoutes
Expand All @@ -61,8 +61,6 @@ class MainActivity : FragmentActivity() {

private val bluetoothViewModel by viewModels<BluetoothViewModel>()

private val logger by speziLogger()

@Inject
@Dispatching.Main
lateinit var mainDispatcher: CoroutineDispatcher
Expand Down Expand Up @@ -149,6 +147,10 @@ class MainActivity : FragmentActivity() {
NotificationSettingScreen()
}

composable<Routes.ContactScreen> {
ContactScreen()
}

composable<Routes.AppScreen> {
AppScreen()
}
Expand Down Expand Up @@ -193,6 +195,10 @@ class MainActivity : FragmentActivity() {
Routes.QuestionnaireScreen(event.questionnaireId)
)

is AppNavigationEvent.ContactScreen -> navHostController.navigate(
Routes.ContactScreen
)

is AccountNavigationEvent.LoginScreen -> navHostController.navigate(
Routes.LoginScreen()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package edu.stanford.bdh.engagehf.contact.data

import com.google.firebase.firestore.DocumentSnapshot
import edu.stanford.spezi.core.design.component.StringResource
import edu.stanford.spezi.modules.contact.model.Contact
import edu.stanford.spezi.modules.contact.model.ContactOption
import edu.stanford.spezi.modules.contact.model.PersonNameComponents
import edu.stanford.spezi.modules.contact.model.call
import edu.stanford.spezi.modules.contact.model.email
import javax.inject.Inject

class ContactDocumentToContactMapper @Inject constructor() {

fun map(document: DocumentSnapshot): Result<Contact> = runCatching {
val contactName = document.getString(CONTACT_NAME_FIELD)
val organisationName = document.getString(ORGANISATION_NAME_FIELD)
if (contactName == null || organisationName == null) {
error("Missing required data, contactName: $contactName, organisation: $organisationName")
}
val components = contactName.split(", ")
val nameComponents = components.firstOrNull()?.split(" ")
val personNameComponents = PersonNameComponents(
givenName = nameComponents?.getOrNull(0),
familyName = nameComponents?.drop(1)
?.joinToString(" ") // assigning everything besides given name here
)
val title = components.lastOrNull()
val contactEmail = document.getString(CONTACT_EMAIL_FIELD)
val phone = document.getString(CONTACT_PHONE_FIELD)
Contact(
name = personNameComponents,
title = title?.let { StringResource(it) },
organization = StringResource(organisationName),
options = listOfNotNull(
phone?.let { ContactOption.call(it) },
contactEmail?.let { ContactOption.email(listOf(contactEmail)) },
),
)
}

private companion object {
const val CONTACT_NAME_FIELD = "contactName"
const val CONTACT_EMAIL_FIELD = "emailAddress"
const val CONTACT_PHONE_FIELD = "phoneNumber"
const val ORGANISATION_NAME_FIELD = "name"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package edu.stanford.bdh.engagehf.contact.data

import com.google.firebase.firestore.FirebaseFirestore
import edu.stanford.spezi.core.logging.speziLogger
import edu.stanford.spezi.module.account.manager.UserSessionManager
import edu.stanford.spezi.modules.contact.model.Contact
import kotlinx.coroutines.tasks.await
import javax.inject.Inject

class EngageContactRepository @Inject constructor(
private val firebaseFirestore: FirebaseFirestore,
private val userSessionManager: UserSessionManager,
private val contactDocumentToContactMapper: ContactDocumentToContactMapper,
) {
private val logger by speziLogger()

suspend fun getContact(): Result<Contact> = runCatching {
val uid = userSessionManager.getUserUid() ?: error("User not available")
val organization = firebaseFirestore
.collection(USERS_PATH)
.document(uid)
.get()
.await()
.getString(ORGANISATION_FIELD) ?: error("Organization not found")
val contactDocument = firebaseFirestore.collection(ORGANISATION_PATH)
.document(organization)
.get()
.await()

contactDocumentToContactMapper.map(contactDocument).fold(
onSuccess = { contact ->
contact
},
onFailure = { error ->
logger.e(error) { "Failed to map contact" }
error("Failed to map contact")
}
)
}

private companion object {
const val ORGANISATION_FIELD = "organization"
const val ORGANISATION_PATH = "organizations"
const val USERS_PATH = "users"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package edu.stanford.bdh.engagehf.contact.ui

import android.location.Address
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.hilt.navigation.compose.hiltViewModel
import edu.stanford.spezi.core.design.component.AppTopAppBar
import edu.stanford.spezi.core.design.component.CenteredBoxContent
import edu.stanford.spezi.core.design.component.StringResource
import edu.stanford.spezi.core.design.theme.Colors.primary
import edu.stanford.spezi.core.design.theme.Spacings
import edu.stanford.spezi.core.design.theme.SpeziTheme
import edu.stanford.spezi.core.design.theme.TextStyles
import edu.stanford.spezi.core.design.theme.ThemePreviews
import edu.stanford.spezi.core.notification.R
import edu.stanford.spezi.modules.contact.ContactComposable
import edu.stanford.spezi.modules.contact.model.Contact
import edu.stanford.spezi.modules.contact.model.ContactOption
import edu.stanford.spezi.modules.contact.model.PersonNameComponents
import edu.stanford.spezi.modules.contact.model.call
import edu.stanford.spezi.modules.contact.model.email
import edu.stanford.spezi.modules.contact.model.website
import java.util.Locale

@Composable
internal fun ContactScreen() {
val viewModel = hiltViewModel<ContactScreenViewModel>()
val uiState by viewModel.uiState.collectAsState()
ContactScreen(
onAction = viewModel::onAction,
uiState = uiState,
)
}

@Composable
private fun ContactScreen(
onAction: (ContactScreenViewModel.Action) -> Unit,
uiState: ContactScreenViewModel.UiState,
) {
Scaffold(topBar = {
AppTopAppBar(title = {
Text(
text = stringResource(edu.stanford.bdh.engagehf.R.string.contact),
)
}, navigationIcon = {
IconButton(onClick = {
onAction(ContactScreenViewModel.Action.Back)
}) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
})
}, content = { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(horizontal = Spacings.medium)
) {
when (uiState) {
is ContactScreenViewModel.UiState.Error -> {
CenteredBoxContent {
Text(
text = uiState.message,
style = TextStyles.headlineMedium,
textAlign = TextAlign.Center,
)
}
}

ContactScreenViewModel.UiState.Loading -> {
CenteredBoxContent {
CircularProgressIndicator(color = primary)
}
}

is ContactScreenViewModel.UiState.ContactLoaded -> {
val contact = uiState.contact
ContactComposable(
contact = contact,
)
}
}
}
})
}

private class ContactUiStateProvider : PreviewParameterProvider<ContactScreenViewModel.UiState> {
override val values = sequenceOf(
ContactScreenViewModel.UiState.Loading,
ContactScreenViewModel.UiState.Error("An error occurred"),
ContactScreenViewModel.UiState.ContactLoaded(
contact = Contact(
name = PersonNameComponents(givenName = "Leland", familyName = "Stanford"),
image = Icons.Default.AccountBox,
title = StringResource("University Founder"),
description = StringResource(
"""Leland Stanford (March 9, 1824 – June 21, 1893) was an American industrialist and politician."""
),
organization = StringResource("Stanford University"),
address = Address(Locale.US).apply {
setAddressLine(0, "450 Jane Stanford Way")
locality = "Stanford"
adminArea = "CA"
},
options = listOf(
ContactOption.call("+49 123 456 789"),
ContactOption.email(listOf("[email protected]")),
ContactOption.website("https://www.google.com")
)
)
)
)
}

@ThemePreviews
@Composable
private fun ContactScreenPreview(
@PreviewParameter(ContactUiStateProvider::class) uiState: ContactScreenViewModel.UiState,
) {
SpeziTheme {
ContactScreen(
onAction = {},
uiState = uiState
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package edu.stanford.bdh.engagehf.contact.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import edu.stanford.bdh.engagehf.contact.data.EngageContactRepository
import edu.stanford.spezi.core.logging.speziLogger
import edu.stanford.spezi.core.navigation.NavigationEvent
import edu.stanford.spezi.core.navigation.Navigator
import edu.stanford.spezi.modules.contact.model.Contact
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
internal class ContactScreenViewModel @Inject constructor(
private val engageContactRepository: EngageContactRepository,
private val navigator: Navigator,
) : ViewModel() {
private val logger by speziLogger()

private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()

init {
loadContact()
}

private fun loadContact() {
viewModelScope.launch {
engageContactRepository.getContact().fold(onSuccess = { contact ->
_uiState.value = UiState.ContactLoaded(contact)
}, onFailure = { error ->
_uiState.value = UiState.Error(error.message ?: "Failed to load contact")
logger.e(error) { "Failed to load contact" }
})
}
}

fun onAction(action: Action) {
when (action) {
Action.Back -> navigator.navigateTo(NavigationEvent.PopBackStack)
}
}

sealed interface Action {
data object Back : Action
}

sealed interface UiState {
data object Loading : UiState
data class Error(val message: String) : UiState
data class ContactLoaded(val contact: Contact) : UiState
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import edu.stanford.spezi.core.navigation.NavigationEvent
sealed interface AppNavigationEvent : NavigationEvent {
data class AppScreen(val clearBackStack: Boolean) : AppNavigationEvent
data class QuestionnaireScreen(val questionnaireId: String) : AppNavigationEvent
data object ContactScreen : AppNavigationEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ sealed class Routes {

@Serializable
data object ConsentScreen : Routes()

@Serializable
data object ContactScreen : Routes()
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) {
style = bodyMedium,
)
}
TextButton(
onClick = {
onAction(Action.ShowContact)
},
modifier = Modifier
.align(Alignment.Start),
) {
Text(
text = stringResource(R.string.contact),
style = bodyMedium,
)
}
HorizontalDivider()
VerticalSpacer()
TextButton(
Expand Down
Loading

0 comments on commit 0148cf3

Please sign in to comment.