diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt index c96994d..3340a31 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt @@ -33,7 +33,11 @@ import be.ugent.sel.studeez.common.ext.defaultButtonShape import be.ugent.sel.studeez.R.string as AppText @Composable -fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { +fun BasicTextButton( + @StringRes text: Int, + modifier: Modifier, + action: () -> Unit +) { TextButton( onClick = action, modifier = modifier diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ProfilePictureComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ProfilePictureComposable.kt new file mode 100644 index 0000000..c214088 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ProfilePictureComposable.kt @@ -0,0 +1,44 @@ +package be.ugent.sel.studeez.common.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.ui.theme.StudeezTheme + +@Composable +fun ProfilePicture() { + Box( + modifier = Modifier + .size(40.dp) + .background(MaterialTheme.colors.primary, CircleShape) + ) { + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(id = R.string.username), + modifier = Modifier + .size(30.dp) + .align(Alignment.Center), + tint = MaterialTheme.colors.onPrimary + ) + } +} + +@Preview +@Composable +fun ProfilePicturePreview() { + StudeezTheme { + ProfilePicture() + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt index 47bdec5..66c7bc4 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt @@ -1,6 +1,7 @@ package be.ugent.sel.studeez.common.composable import androidx.annotation.StringRes +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions @@ -9,6 +10,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -21,6 +23,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import be.ugent.sel.studeez.common.ext.fieldModifier import be.ugent.sel.studeez.resources +import com.google.android.material.color.MaterialColors +import kotlin.math.sin import be.ugent.sel.studeez.R.drawable as AppIcon import be.ugent.sel.studeez.R.string as AppText @@ -215,4 +219,34 @@ private fun PasswordField( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), visualTransformation = visualTransformation ) +} + +@Composable +fun SearchField( + value: String, + onValueChange: (String) -> Unit, + onSubmit: () -> Unit, + @StringRes label: Int, + modifier: Modifier = Modifier +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = { Text(text = stringResource(id = label)) }, + trailingIcon = { + IconButton(onClick = onSubmit) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(label), + tint = MaterialTheme.colors.primary + ) + } + }, + singleLine = true, + colors = TextFieldDefaults.outlinedTextFieldColors( + textColor = MaterialTheme.colors.onBackground, + backgroundColor = MaterialTheme.colors.background + ) + ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt b/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt index c52939f..ebe8589 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt @@ -5,6 +5,7 @@ import be.ugent.sel.studeez.data.local.models.task.Subject import be.ugent.sel.studeez.data.local.models.task.Task import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo +import be.ugent.sel.studeez.domain.UserDAO import javax.inject.Inject import javax.inject.Singleton @@ -42,4 +43,11 @@ class SelectedSubject @Inject constructor() : SelectedState() { @Singleton class SelectedTimerInfo @Inject constructor() : SelectedState() { override lateinit var value: TimerInfo +} + +@Singleton +class SelectedUserId @Inject constructor( + userDAO: UserDAO +): SelectedState() { + override var value: String = userDAO.getCurrentUserId() } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/Friendship.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/Friendship.kt new file mode 100644 index 0000000..98aa9a5 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/Friendship.kt @@ -0,0 +1,11 @@ +package be.ugent.sel.studeez.data.local.models + +import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentId + +data class Friendship( + @DocumentId val id: String = "", + val friendId: String = "", + val friendsSince: Timestamp = Timestamp.now(), + val accepted: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt index 2fba2ce..a92bebb 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt @@ -1,3 +1,9 @@ package be.ugent.sel.studeez.data.local.models -data class User(val id: String = "") +import com.google.firebase.firestore.DocumentId + +data class User( + @DocumentId val id: String = "", + val username: String = "", + val biography: String = "" +) diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseFriendship.kt b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseFriendship.kt new file mode 100644 index 0000000..fb2af4b --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseFriendship.kt @@ -0,0 +1,7 @@ +package be.ugent.sel.studeez.data.remote + +object FirebaseFriendship { + const val FRIENDID: String = "friendId" + const val ACCEPTED: String = "accepted" + const val FRIENDSSINCE: String = "friendsSince" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseSessionReport.kt b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseSessionReport.kt new file mode 100644 index 0000000..f33718f --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseSessionReport.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.data.remote + +object FirebaseSessionReport { + const val STUDYTIME: String = "studyTime" + const val ENDTIME: String = "endTime" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseUser.kt b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseUser.kt new file mode 100644 index 0000000..9ee5aa2 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseUser.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.data.remote + +object FirebaseUser { + const val USERNAME: String = "username" + const val BIOGRAPHY: String = "biography" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt b/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt index 4c5fea1..33bf73b 100644 --- a/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt +++ b/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt @@ -16,6 +16,9 @@ abstract class DatabaseModule { @Binds abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO + @Binds + abstract fun provideFriendshipDAO(impl: FirebaseFriendshipDAO): FriendshipDAO + @Binds abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO @@ -26,13 +29,13 @@ abstract class DatabaseModule { abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService @Binds - abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO + abstract fun provideSessionDAO(impl: FirebaseSessionDAO): SessionDAO @Binds - abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO + abstract fun provideSubjectDAO(impl: FirebaseSubjectDAO): SubjectDAO @Binds - abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO + abstract fun provideTaskDAO(impl: FirebaseTaskDAO): TaskDAO @Binds abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/FriendshipDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/FriendshipDAO.kt new file mode 100644 index 0000000..0beb01a --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/FriendshipDAO.kt @@ -0,0 +1,54 @@ +package be.ugent.sel.studeez.domain + +import be.ugent.sel.studeez.data.local.models.Friendship +import kotlinx.coroutines.flow.Flow + +/** + * Should be used for interactions between friends. + */ +interface FriendshipDAO { + + /** + * @return all friendships of a chosen user. + */ + fun getAllFriendships( + userId: String + ): Flow> + + /** + * @return the amount of friends of a chosen user. + * This method should be faster than just counting the length of getAllFriends() + */ + fun getFriendshipCount( + userId: String + ): Flow + + /** + * @param id the id of the friendship that you want details of + * @return the details of a Friendship + */ + fun getFriendshipDetails(id: String): Friendship + + /** + * Send a friend request to a user. + * @param id of the user that you want to add as a friend + * @return Success/faillure of transaction + */ + fun sendFriendshipRequest(id: String): Boolean + + /** + * Accept a friend request that has already been sent. + * @param id of the friendship that you want to update + * @return: Success/faillure of transaction + */ + fun acceptFriendship(id: String): Boolean + + /** + * Remove a friend or decline a friendrequest. + * @param friendship the one you want to remove + * @return: Success/faillure of transaction + */ + fun removeFriendship( + friendship: Friendship + ): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt index 77087d2..bb233e9 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt @@ -1,12 +1,19 @@ package be.ugent.sel.studeez.domain import be.ugent.sel.studeez.data.local.models.SessionReport +import be.ugent.sel.studeez.data.local.models.User import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import kotlinx.coroutines.flow.Flow interface SessionDAO { fun getSessions(): Flow> + suspend fun getSessionsOfUser(userId: String): List + + /** + * Return a list of pairs, containing the username and all the studysessions of that user. + */ + fun getFriendsSessions(): Flow>>> fun saveSession(newSessionReport: SessionReport) diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt index b96cf17..80a7689 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt @@ -1,13 +1,52 @@ package be.ugent.sel.studeez.domain +import be.ugent.sel.studeez.data.local.models.User +import kotlinx.coroutines.flow.Flow + interface UserDAO { - suspend fun getUsername(): String? - suspend fun save(newUsername: String) + fun getCurrentUserId(): String /** - * Delete all references to this user in the database. Similar to the deleteCascade in + * @return all users + */ + fun getAllUsers(): Flow> + + /** + * @return all users based on a query, a trimmed down version of getAllUsers() + */ + fun getUsersWithQuery( + fieldName: String, + value: String + ): Flow> + + /** + * Request information about a user + */ + fun getUserDetails( + userId: String + ): Flow + + suspend fun getUsername( + userId: String + ): String + + /** + * @return information on the currently logged in user. + */ + suspend fun getLoggedInUser(): User + // TODO Should be refactored to fun getLoggedInUser(): Flow, without suspend. + + suspend fun saveLoggedInUser( + newUsername: String, + newBiography: String = "" + ) + // TODO Should be refactored to fun saveLoggedInUser(...): Boolean, without suspend. + + /** + * Delete all references to the logged in user in the database. Similar to the deleteCascade in * relational databases. */ - suspend fun deleteUserReferences() + suspend fun deleteLoggedInUserReferences() + // TODO Should be refactored to fun deleteLoggedInUserReferences(): Boolean, without suspend. } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSessionDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSessionDAO.kt deleted file mode 100644 index a818236..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSessionDAO.kt +++ /dev/null @@ -1,37 +0,0 @@ -package be.ugent.sel.studeez.domain.implementation - -import be.ugent.sel.studeez.data.local.models.SessionReport -import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo -import be.ugent.sel.studeez.domain.AccountDAO -import be.ugent.sel.studeez.domain.SessionDAO -import com.google.firebase.firestore.CollectionReference -import com.google.firebase.firestore.FirebaseFirestore -import com.google.firebase.firestore.ktx.snapshots -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -class FireBaseSessionDAO @Inject constructor( - private val firestore: FirebaseFirestore, - private val auth: AccountDAO -) : SessionDAO { - - override fun getSessions(): Flow> { - return currentUserSessionsCollection() - .snapshots() - .map { it.toObjects(SessionReport::class.java) } - } - - override fun saveSession(newSessionReport: SessionReport) { - currentUserSessionsCollection().add(newSessionReport) - } - - override fun deleteSession(newTimer: TimerInfo) { - currentUserSessionsCollection().document(newTimer.id).delete() - } - - private fun currentUserSessionsCollection(): CollectionReference = - firestore.collection(FireBaseCollections.USER_COLLECTION) - .document(auth.currentUserId) - .collection(FireBaseCollections.SESSION_COLLECTION) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseCollections.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseCollections.kt similarity index 78% rename from app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseCollections.kt rename to app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseCollections.kt index 78867c9..042c0f0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseCollections.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseCollections.kt @@ -1,8 +1,9 @@ package be.ugent.sel.studeez.domain.implementation -object FireBaseCollections { +object FirebaseCollections { const val SESSION_COLLECTION = "sessions" const val USER_COLLECTION = "users" + const val FRIENDS_COLLECTION = "friends" const val TIMER_COLLECTION = "timers" const val SUBJECT_COLLECTION = "subjects" const val TASK_COLLECTION = "tasks" diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFriendshipDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFriendshipDAO.kt new file mode 100644 index 0000000..bd429e1 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFriendshipDAO.kt @@ -0,0 +1,134 @@ +package be.ugent.sel.studeez.domain.implementation + +import androidx.compose.runtime.collectAsState +import be.ugent.sel.studeez.common.snackbar.SnackbarManager +import be.ugent.sel.studeez.data.local.models.Friendship +import be.ugent.sel.studeez.data.remote.FirebaseFriendship.ACCEPTED +import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDSSINCE +import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDID +import be.ugent.sel.studeez.domain.AccountDAO +import be.ugent.sel.studeez.domain.FriendshipDAO +import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.FRIENDS_COLLECTION +import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION +import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.snapshots +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import be.ugent.sel.studeez.R.string as AppText + +class FirebaseFriendshipDAO @Inject constructor( + private val firestore: FirebaseFirestore, + private val auth: AccountDAO +): FriendshipDAO { + + private fun currentUserDocument(): DocumentReference = firestore + .collection(USER_COLLECTION) + .document(auth.currentUserId) + + override fun getAllFriendships( + userId: String + ): Flow> { + return firestore + .collection(USER_COLLECTION) + .document(userId) + .collection(FRIENDS_COLLECTION) + .snapshots() + .map { it.toObjects(Friendship::class.java) } + } + + override fun getFriendshipCount( + userId: String + ): Flow { + return flow { + val friendshipCount = suspendCoroutine { continuation -> + firestore + .collection(USER_COLLECTION) + .document(userId) + .collection(FRIENDS_COLLECTION) + .get() + .addOnSuccessListener { querySnapshot -> + continuation.resume(querySnapshot.size()) + } + .addOnFailureListener { exception -> + continuation.resumeWithException(exception) + } + } + emit(friendshipCount) + }.catch { + SnackbarManager.showMessage(AppText.generic_error) + } + } + + override fun getFriendshipDetails(id: String): Friendship { + TODO("Not yet implemented") + } + + override fun sendFriendshipRequest(id: String): Boolean { + val currentUserId: String = auth.currentUserId + val otherUserId: String = id + + // Add entry to current user + currentUserDocument() + .collection(FRIENDS_COLLECTION) + .add(mapOf( + FRIENDID to otherUserId, + ACCEPTED to true, // TODO Make it not automatically accepted. + FRIENDSSINCE to Timestamp.now() + )) + + // Add entry to other user + firestore.collection(USER_COLLECTION) + .document(otherUserId) + .collection(FRIENDS_COLLECTION) + .add(mapOf( + FRIENDID to currentUserId, + ACCEPTED to true, // TODO Make it not automatically accepted. + FRIENDSSINCE to Timestamp.now() + )) + + return true + } + + override fun acceptFriendship(id: String): Boolean { + TODO("Not yet implemented") + } + + override fun removeFriendship( + friendship: Friendship + ): Boolean { + val currentUserId: String = auth.currentUserId + val otherUserId: String = friendship.friendId + + // Remove at logged in user + firestore.collection(USER_COLLECTION) + .document(currentUserId) + .collection(FRIENDS_COLLECTION) + .document(friendship.id) + .delete() + + // Remove at other user + firestore.collection(USER_COLLECTION) + .document(otherUserId) + .collection(FRIENDS_COLLECTION) + .whereEqualTo(FRIENDID, currentUserId) + .get() + .addOnSuccessListener { + for (document in it) { + document.reference.delete() + } + }.addOnFailureListener { + SnackbarManager.showMessage(AppText.generic_error) + } + + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSessionDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSessionDAO.kt new file mode 100644 index 0000000..e7cb763 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSessionDAO.kt @@ -0,0 +1,79 @@ +package be.ugent.sel.studeez.domain.implementation + +import be.ugent.sel.studeez.data.local.models.SessionReport +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo +import be.ugent.sel.studeez.data.remote.FirebaseSessionReport +import be.ugent.sel.studeez.data.remote.FirebaseSessionReport.ENDTIME +import be.ugent.sel.studeez.data.remote.FirebaseSessionReport.STUDYTIME +import be.ugent.sel.studeez.domain.AccountDAO +import be.ugent.sel.studeez.domain.FriendshipDAO +import be.ugent.sel.studeez.domain.SessionDAO +import be.ugent.sel.studeez.domain.UserDAO +import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.SESSION_COLLECTION +import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION +import com.google.firebase.Timestamp +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.getField +import com.google.firebase.firestore.ktx.snapshots +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.tasks.await +import javax.inject.Inject + +class FirebaseSessionDAO @Inject constructor( + private val firestore: FirebaseFirestore, + private val auth: AccountDAO, + private val userDAO: UserDAO, + private val friendshipDAO: FriendshipDAO +) : SessionDAO { + + override fun getSessions(): Flow> { + return currentUserSessionsCollection() + .snapshots() + .map { it.toObjects(SessionReport::class.java) } + } + + override suspend fun getSessionsOfUser(userId: String): List { + val collection = firestore.collection(USER_COLLECTION) + .document(userId) + .collection(SESSION_COLLECTION) + .get().await() + val list: MutableList = mutableListOf() + for (document in collection) { + val id = document.id + val studyTime: Int = document.getField(STUDYTIME)!! + val endTime: Timestamp = document.getField(ENDTIME)!! + list.add(SessionReport(id, studyTime, endTime)) + } + return list + } + + override fun getFriendsSessions(): Flow>>> { + return friendshipDAO.getAllFriendships(auth.currentUserId) + .map { friendships -> + friendships.map { friendship -> + val userId: String = friendship.friendId + val username = userDAO.getUsername(userId) + val userSessions = getSessionsOfUser(userId) + + Pair(username, userSessions) + } + } + } + + override fun saveSession(newSessionReport: SessionReport) { + currentUserSessionsCollection().add(newSessionReport) + } + + override fun deleteSession(newTimer: TimerInfo) { + currentUserSessionsCollection().document(newTimer.id).delete() + } + + private fun currentUserSessionsCollection(): CollectionReference = + firestore.collection(USER_COLLECTION) + .document(auth.currentUserId) + .collection(SESSION_COLLECTION) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSubjectDAO.kt similarity index 88% rename from app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt rename to app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSubjectDAO.kt index 915a7f9..74c5d38 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSubjectDAO.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.tasks.await import javax.inject.Inject import kotlin.collections.count -class FireBaseSubjectDAO @Inject constructor( +class FirebaseSubjectDAO @Inject constructor( private val firestore: FirebaseFirestore, private val auth: AccountDAO, private val taskDAO: TaskDAO, @@ -49,7 +49,7 @@ class FireBaseSubjectDAO @Inject constructor( override suspend fun archiveSubject(subject: Subject) { currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true) currentUserSubjectsCollection().document(subject.id) - .collection(FireBaseCollections.TASK_COLLECTION) + .collection(FirebaseCollections.TASK_COLLECTION) .taskNotArchived() .get().await() .documents @@ -74,16 +74,16 @@ class FireBaseSubjectDAO @Inject constructor( } private fun currentUserSubjectsCollection(): CollectionReference = - firestore.collection(FireBaseCollections.USER_COLLECTION) + firestore.collection(FirebaseCollections.USER_COLLECTION) .document(auth.currentUserId) - .collection(FireBaseCollections.SUBJECT_COLLECTION) + .collection(FirebaseCollections.SUBJECT_COLLECTION) private fun subjectTasksCollection(subject: Subject): CollectionReference = - firestore.collection(FireBaseCollections.USER_COLLECTION) + firestore.collection(FirebaseCollections.USER_COLLECTION) .document(auth.currentUserId) - .collection(FireBaseCollections.SUBJECT_COLLECTION) + .collection(FirebaseCollections.SUBJECT_COLLECTION) .document(subject.id) - .collection(FireBaseCollections.TASK_COLLECTION) + .collection(FirebaseCollections.TASK_COLLECTION) fun CollectionReference.subjectNotArchived(): Query = this.whereEqualTo(SubjectDocument.archived, false) diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseTaskDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTaskDAO.kt similarity index 90% rename from app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseTaskDAO.kt rename to app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTaskDAO.kt index 685b237..93bc221 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseTaskDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTaskDAO.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.tasks.await import javax.inject.Inject -class FireBaseTaskDAO @Inject constructor( +class FirebaseTaskDAO @Inject constructor( private val firestore: FirebaseFirestore, private val auth: AccountDAO, ) : TaskDAO { @@ -45,12 +45,11 @@ class FireBaseTaskDAO @Inject constructor( } private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference = - firestore.collection(FireBaseCollections.USER_COLLECTION) + firestore.collection(FirebaseCollections.USER_COLLECTION) .document(auth.currentUserId) - .collection(FireBaseCollections.SUBJECT_COLLECTION) + .collection(FirebaseCollections.SUBJECT_COLLECTION) .document(subjectId) - .collection(FireBaseCollections.TASK_COLLECTION) - + .collection(FirebaseCollections.TASK_COLLECTION) } // Extend CollectionReference and Query with some filters diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt index 1f37a18..dad7047 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt @@ -48,8 +48,8 @@ class FirebaseTimerDAO @Inject constructor( } private fun currentUserTimersCollection(): CollectionReference = - firestore.collection(FireBaseCollections.USER_COLLECTION) + firestore.collection(FirebaseCollections.USER_COLLECTION) .document(auth.currentUserId) - .collection(FireBaseCollections.TIMER_COLLECTION) + .collection(FirebaseCollections.TIMER_COLLECTION) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt index 3158b88..04239c0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt @@ -2,34 +2,91 @@ package be.ugent.sel.studeez.domain.implementation import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.snackbar.SnackbarManager +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.data.remote.FirebaseUser.BIOGRAPHY +import be.ugent.sel.studeez.data.remote.FirebaseUser.USERNAME import be.ugent.sel.studeez.domain.AccountDAO import be.ugent.sel.studeez.domain.UserDAO +import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.snapshots +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.tasks.await import javax.inject.Inject class FirebaseUserDAO @Inject constructor( private val firestore: FirebaseFirestore, private val auth: AccountDAO - ) : UserDAO { +) : UserDAO { - override suspend fun getUsername(): String? { - return currentUserDocument().get().await().getString("username") - } - - override suspend fun save(newUsername: String) { - currentUserDocument().set(mapOf("username" to newUsername)) + override fun getCurrentUserId(): String { + return auth.currentUserId } private fun currentUserDocument(): DocumentReference = - firestore.collection(USER_COLLECTION).document(auth.currentUserId) + firestore + .collection(USER_COLLECTION) + .document(auth.currentUserId) - companion object { - private const val USER_COLLECTION = "users" + override fun getAllUsers(): Flow> { + return firestore + .collection(USER_COLLECTION) + .snapshots() + .map { it.toObjects(User::class.java) } } - override suspend fun deleteUserReferences() { + override fun getUsersWithQuery( + fieldName: String, + value: String + ): Flow> { + return firestore + .collection(USER_COLLECTION) + .whereEqualTo(fieldName, value) + .snapshots() + .map { it.toObjects(User::class.java) } + } + + override fun getUserDetails(userId: String): Flow { + return flow { + val snapshot = firestore + .collection(USER_COLLECTION) + .document(userId) + .get() + .await() + val user = snapshot.toObject(User::class.java)!! + emit(user) + } + } + + override suspend fun getUsername(userId: String): String { + val user = firestore.collection(USER_COLLECTION) + .document(userId) + .get().await() + return user.getString(USERNAME)!! + } + + override suspend fun getLoggedInUser(): User { + val userDocument = currentUserDocument().get().await() + return User( + username = userDocument.getString(USERNAME) ?: "", + biography = userDocument.getString(BIOGRAPHY) ?: "" + ) + } + + override suspend fun saveLoggedInUser( + newUsername: String, + newBiography: String + ) { + currentUserDocument().set(mapOf( + USERNAME to newUsername, + BIOGRAPHY to newBiography + )) + } + + override suspend fun deleteLoggedInUserReferences() { currentUserDocument().delete() .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } diff --git a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt index 49856c9..578c74a 100644 --- a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt +++ b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt @@ -30,7 +30,9 @@ object StudeezDestinations { const val EDIT_TASK_FORM = "edit_task" // Friends flow + const val FRIENDS_OVERVIEW_SCREEN = "friends_overview" const val SEARCH_FRIENDS_SCREEN = "search_friends" + const val PUBLIC_PROFILE_SCREEN = "public_profile" // Create & edit screens const val CREATE_TASK_SCREEN = "create_task" diff --git a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt index 0a0e8b4..6b59abc 100644 --- a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt +++ b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt @@ -14,10 +14,13 @@ import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions +import be.ugent.sel.studeez.screens.friends.friends_overview.FriendsOveriewRoute +import be.ugent.sel.studeez.screens.friends.friends_search.SearchFriendsRoute import be.ugent.sel.studeez.screens.home.HomeRoute import be.ugent.sel.studeez.screens.log_in.LoginRoute -import be.ugent.sel.studeez.screens.profile.EditProfileRoute +import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileRoute import be.ugent.sel.studeez.screens.profile.ProfileRoute +import be.ugent.sel.studeez.screens.profile.public_profile.PublicProfileRoute import be.ugent.sel.studeez.screens.session.SessionRoute import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute import be.ugent.sel.studeez.screens.sessions.SessionsRoute @@ -69,6 +72,7 @@ fun StudeezNavGraph( drawerActions = drawerActions, navigationBarActions = navigationBarActions, feedViewModel = hiltViewModel(), + viewModel = hiltViewModel() ) } @@ -221,8 +225,28 @@ fun StudeezNavGraph( } // Friends flow + composable(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) { + FriendsOveriewRoute( + open = open, + popUp = goBack, + viewModel = hiltViewModel() + ) + } + composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) { - // TODO + SearchFriendsRoute( + popUp = goBack, + open = open, + viewModel = hiltViewModel() + ) + } + + composable(StudeezDestinations.PUBLIC_PROFILE_SCREEN) { + PublicProfileRoute( + popUp = goBack, + open = open, + viewModel = hiltViewModel() + ) } // Create & edit screens diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewScreen.kt new file mode 100644 index 0000000..8ea6d20 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewScreen.kt @@ -0,0 +1,288 @@ +package be.ugent.sel.studeez.screens.friends.friends_overview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.common.composable.BasicButton +import be.ugent.sel.studeez.common.composable.ProfilePicture +import be.ugent.sel.studeez.common.composable.SearchField +import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry +import be.ugent.sel.studeez.common.ext.basicButton +import be.ugent.sel.studeez.data.local.models.Friendship +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.resources +import be.ugent.sel.studeez.ui.theme.StudeezTheme +import com.google.firebase.Timestamp +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import be.ugent.sel.studeez.R.string as AppText + +data class FriendsOverviewActions( + val getFriendsFlow: () -> Flow>>, + val searchFriends: () -> Unit, + val onQueryStringChange: (String) -> Unit, + val onSubmit: () -> Unit, + val viewProfile: (String) -> Unit, + val removeFriend: (Friendship) -> Unit +) + +fun getFriendsOverviewActions( + viewModel: FriendsOverviewViewModel, + open: (String) -> Unit +): FriendsOverviewActions { + return FriendsOverviewActions( + getFriendsFlow = viewModel::getAllFriends, + searchFriends = { viewModel.searchFriends(open) }, + onQueryStringChange = viewModel::onQueryStringChange, + onSubmit = { viewModel.onSubmit(open) }, + viewProfile = { userId -> + viewModel.viewProfile(userId, open) + }, + removeFriend = viewModel::removeFriend + ) +} + +@Composable +fun FriendsOveriewRoute( + open: (String) -> Unit, + popUp: () -> Unit, + viewModel: FriendsOverviewViewModel +) { + val uiState by viewModel.uiState + FriendsOverviewScreen( + popUp = popUp, + uiState = uiState, + friendsOverviewActions = getFriendsOverviewActions( + viewModel = viewModel, + open = open + ) + ) +} + +@Composable +fun FriendsOverviewScreen( + popUp: () -> Unit, + uiState: FriendsOverviewUiState, + friendsOverviewActions: FriendsOverviewActions +) { + val friends = friendsOverviewActions.getFriendsFlow().collectAsState(initial = emptyList()) + + Scaffold( + topBar = { + TopAppBar( + title = { + // TODO Link to each other + SearchField( + value = uiState.queryString, + onValueChange = friendsOverviewActions.onQueryStringChange, + onSubmit = friendsOverviewActions.onSubmit, + label = AppText.search_friends + ) + }, + navigationIcon = { + IconButton(onClick = popUp) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = resources().getString(R.string.go_back) + ) + } + } + // TODO Add inbox action + ) + } + ) { paddingValues -> + LazyColumn ( + modifier = Modifier.padding(paddingValues) + ) { + if (friends.value.isEmpty()) { + // Show a quick button to search friends when the user does not have any friends yet. + item { + BasicButton( + text = AppText.no_friends, + modifier = Modifier.basicButton() + ) { + friendsOverviewActions.searchFriends() + } + } + } + + items(friends.value) { friend -> + FriendsEntry( + user = friend.first, + friendship = friend.second, + viewProfile = { userId -> friendsOverviewActions.viewProfile(userId) }, + removeFriend = friendsOverviewActions.removeFriend + ) + } + } + } +} + +@Preview +@Composable +fun FriendsOverviewPreview() { + StudeezTheme { + FriendsOverviewScreen( + popUp = {}, + uiState = FriendsOverviewUiState(""), + friendsOverviewActions = FriendsOverviewActions( + getFriendsFlow = { emptyFlow() }, + searchFriends = {}, + onQueryStringChange = {}, + onSubmit = {}, + viewProfile = {}, + removeFriend = {} + ) + ) + } +} + +@Composable +fun FriendsEntry( + user: User, + friendship: Friendship, + viewProfile: (String) -> Unit, + removeFriend: (Friendship) -> Unit +) { + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp, vertical = 7.dp), + ) { + Box( + modifier = Modifier + .padding(vertical = 4.dp) + ) { + ProfilePicture() + } + + Box ( + modifier = Modifier + .fillMaxWidth() + ) { + Column ( + modifier = Modifier + .padding(vertical = 4.dp) + ) { + Text( + text = user.username, + fontSize = 16.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}", + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd + ) { + FriendsOverviewDropDown( + friendship = friendship, + viewProfile = viewProfile, + removeFriend = removeFriend + ) + } + } + } +} + +@Preview +@Composable +fun FriendsEntryPreview() { + StudeezTheme { + FriendsEntry( + user = User( + id = "", + username = "Tibo De Peuter", + biography = "short bio" + ), + friendship = Friendship( + id = "", + friendId = "someId", + friendsSince = Timestamp.now(), + accepted = true + ), + viewProfile = {}, + removeFriend = {} + ) + } +} + +@Composable +fun FriendsOverviewDropDown( + friendship: Friendship, + viewProfile: (String) -> Unit, + removeFriend: (Friendship) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { expanded = true } + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal), + contentDescription = resources().getString(AppText.view_more), + modifier = Modifier.fillMaxSize() + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DrawerEntry( + icon = Icons.Default.Person, + text = stringResource(id = AppText.show_profile) + ) { + viewProfile(friendship.friendId) + } + DrawerEntry( + icon = Icons.Default.Delete, + text = stringResource(id = AppText.remove_friend) + ) { + removeFriend(friendship) + expanded = false + } + } +} + +@Preview +@Composable +fun FriendsOverviewDropDownPreview() { + StudeezTheme { + FriendsOverviewDropDown( + friendship = Friendship( + id = "", + friendId = "someId", + friendsSince = Timestamp.now(), + accepted = true + ), + viewProfile = {}, + removeFriend = { } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewUiState.kt new file mode 100644 index 0000000..8672814 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewUiState.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.screens.friends.friends_overview + +data class FriendsOverviewUiState( + val userId: String, + val queryString: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewViewModel.kt new file mode 100644 index 0000000..556e435 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewViewModel.kt @@ -0,0 +1,78 @@ +package be.ugent.sel.studeez.screens.friends.friends_overview + +import androidx.compose.runtime.mutableStateOf +import be.ugent.sel.studeez.data.SelectedUserId +import be.ugent.sel.studeez.data.local.models.Friendship +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.domain.FriendshipDAO +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.domain.UserDAO +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapConcat +import javax.inject.Inject + +@HiltViewModel +class FriendsOverviewViewModel @Inject constructor( + private val userDAO: UserDAO, + private val friendshipDAO: FriendshipDAO, + private val selectedUserIdState: SelectedUserId, + logService: LogService +) : StudeezViewModel(logService) { + + var uiState = mutableStateOf(FriendsOverviewUiState( + userId = selectedUserIdState.value + )) + private set + + fun getAllFriends(): Flow>> { + return friendshipDAO.getAllFriendships( + userId = uiState.value.userId + ) + .flatMapConcat { friendships -> + val userFlows = friendships.map { friendship -> + userDAO.getUserDetails(friendship.friendId) + } + combine(userFlows) { users -> + friendships.zip(users) { friendship, user -> + Pair(user, friendship) + } + } + } + } + + fun searchFriends(open: (String) -> Unit) { + open(StudeezDestinations.SEARCH_FRIENDS_SCREEN) + } + + fun onQueryStringChange(newValue: String) { + uiState.value = uiState.value.copy( + queryString = newValue + ) + } + + fun onSubmit(open: (String) -> Unit) { + val query = uiState.value.queryString // TODO Pass as argument + open(StudeezDestinations.SEARCH_FRIENDS_SCREEN) + } + + fun viewProfile( + userId: String, + open: (String) -> Unit + ) { + selectedUserIdState.value = userId + open(StudeezDestinations.PUBLIC_PROFILE_SCREEN) + } + + fun removeFriend( + friendship: Friendship + ) { + friendshipDAO.removeFriendship( + friendship = friendship + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendUiState.kt new file mode 100644 index 0000000..0a5a10f --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendUiState.kt @@ -0,0 +1,10 @@ +package be.ugent.sel.studeez.screens.friends.friends_search + +import be.ugent.sel.studeez.data.local.models.User +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +data class SearchFriendUiState( + val queryString: String = "", + val searchResults: Flow> = emptyFlow() +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsScreen.kt new file mode 100644 index 0000000..e84bb9f --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsScreen.kt @@ -0,0 +1,265 @@ +package be.ugent.sel.studeez.screens.friends.friends_search + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.common.composable.ProfilePicture +import be.ugent.sel.studeez.common.composable.SearchField +import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.resources +import be.ugent.sel.studeez.ui.theme.StudeezTheme +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import be.ugent.sel.studeez.R.string as AppText + +data class SearchFriendsActions( + val onQueryStringChange: (String) -> Unit, + val getUsersWithUsername: (String) -> Unit, + val getAllUsers: () -> Flow>, + val goToProfile: (String) -> Unit +) + +fun getSearchFriendsActions( + viewModel: SearchFriendsViewModel, + open: (String) -> Unit +): SearchFriendsActions { + return SearchFriendsActions( + onQueryStringChange = viewModel::onQueryStringChange, + getUsersWithUsername = viewModel::getUsersWithUsername, + getAllUsers = { viewModel.getAllUsers() }, + goToProfile = { userId -> viewModel.goToProfile(userId, open) } + ) +} + +@Composable +fun SearchFriendsRoute( + popUp: () -> Unit, + open: (String) -> Unit, + viewModel: SearchFriendsViewModel +) { + val uiState by viewModel.uiState + + SearchFriendsScreen( + popUp = popUp, + uiState = uiState, + searchFriendsActions = getSearchFriendsActions( + viewModel = viewModel, + open = open + ) + ) +} + +@Composable +fun SearchFriendsScreen( + popUp: () -> Unit, + uiState: SearchFriendUiState, + searchFriendsActions: SearchFriendsActions +) { + var query by remember { mutableStateOf(uiState.queryString) } + val searchResults = searchFriendsActions.getAllUsers().collectAsState( + initial = emptyList() + ) + + Scaffold( + topBar = { + TopAppBar( + title = { + SearchField( + value = query, + onValueChange = { newValue -> + searchFriendsActions.onQueryStringChange(newValue) + query = newValue + }, + onSubmit = { }, + label = AppText.search_friends + ) + }, + navigationIcon = { + IconButton(onClick = popUp) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = resources().getString(R.string.go_back) + ) + } + } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues) + ) { + items (searchResults.value) { user -> + UserEntry( + user = user, + goToProfile = searchFriendsActions.goToProfile + ) + } + } + } +} + +@Preview +@Composable +fun SearchFriendsPreview() { + StudeezTheme { + SearchFriendsScreen( + popUp = {}, + uiState = SearchFriendUiState( + queryString = "dit is een test", + searchResults = flowOf(listOf(User( + id = "someid", + username = "Eerste user", + biography = "blah blah blah" + ))) + ), + searchFriendsActions = SearchFriendsActions( + onQueryStringChange = {}, + getUsersWithUsername = {}, + getAllUsers = { + flowOf(listOf(User( + id = "someid", + username = "Eerste user", + biography = "blah blah blah" + ))) + }, + goToProfile = { } + ) + ) + } +} + +@Composable +fun UserEntry( + user: User, + goToProfile: (String) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp, vertical = 7.dp), + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + Box( + modifier = Modifier + .padding(vertical = 4.dp) + ) { + ProfilePicture() + } + + Box ( + modifier = Modifier + .fillMaxWidth() + ) { + Column ( + modifier = Modifier + .padding(vertical = 4.dp) + ) { + Text( + text = user.username, + fontSize = 16.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}", + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd + ) { + SearchFriendsDropDown( + user = user, + goToProfile = goToProfile + ) + } + } + } +} + +@Preview +@Composable +fun UserEntryPreview() { + StudeezTheme { + UserEntry( + user = User( + id = "someid", + username = "Eerste user", + biography = "blah blah blah" + ), + goToProfile = { } + ) + } +} + +/** + * Three dots that open a dropdown menu that allow to go the users profile. + */ +@Composable +fun SearchFriendsDropDown( + user: User, + goToProfile: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { expanded = true } + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal), + contentDescription = stringResource(AppText.view_more), + modifier = Modifier.fillMaxSize() + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem(onClick = { expanded = false }) { + DrawerEntry( + icon = Icons.Default.Person, + text = stringResource(id = AppText.show_profile) + ) { + goToProfile(user.id) + } + } + } +} + +@Preview +@Composable +fun SearchFriendsDropDownPreview() { + StudeezTheme { + SearchFriendsDropDown( + user = User( + id = "someid", + username = "Eerste user", + biography = "blah blah blah" + ), + goToProfile = { } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsViewModel.kt new file mode 100644 index 0000000..39aabf6 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsViewModel.kt @@ -0,0 +1,66 @@ +package be.ugent.sel.studeez.screens.friends.friends_search + +import androidx.compose.runtime.mutableStateOf +import be.ugent.sel.studeez.data.SelectedUserId +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.data.remote.FirebaseUser +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.domain.UserDAO +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import javax.inject.Inject + +@HiltViewModel +class SearchFriendsViewModel @Inject constructor( + private val userDAO: UserDAO, + private val selectedProfileState: SelectedUserId, + logService: LogService +): StudeezViewModel(logService) { + + var uiState = mutableStateOf(SearchFriendUiState()) + private set + + fun onQueryStringChange(newValue: String) { + uiState.value = uiState.value.copy( + queryString = newValue + ) + uiState.value = uiState.value.copy( + searchResults = userDAO.getUsersWithQuery( + fieldName = FirebaseUser.USERNAME, + value = uiState.value.queryString + ) + ) + } + + fun getUsersWithUsername( + value: String + ): Flow> { + return userDAO.getUsersWithQuery( + fieldName = FirebaseUser.USERNAME, + value = value + ) + } + + /** + * Get all users, except for the current user. + */ + fun getAllUsers(): Flow> { + return userDAO.getAllUsers() + .filter { users -> + users.any { user -> + user.id != userDAO.getCurrentUserId() + } + } + } + + fun goToProfile( + userId: String, + open: (String) -> Unit + ) { + selectedProfileState.value = userId + open(StudeezDestinations.PUBLIC_PROFILE_SCREEN) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt index c93527b..7b46c7d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt @@ -21,14 +21,15 @@ import be.ugent.sel.studeez.resources @Composable fun HomeRoute( open: (String) -> Unit, + viewModel: HomeViewModel, drawerActions: DrawerActions, navigationBarActions: NavigationBarActions, feedViewModel: FeedViewModel, ) { val feedUiState by feedViewModel.uiState.collectAsState() HomeScreen( + onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, drawerActions = drawerActions, - open = open, navigationBarActions = navigationBarActions, feedUiState = feedUiState, continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, @@ -38,7 +39,7 @@ fun HomeRoute( @Composable fun HomeScreen( - open: (String) -> Unit, + onViewFriendsClick: () -> Unit, drawerActions: DrawerActions, navigationBarActions: NavigationBarActions, feedUiState: FeedUiState, @@ -49,15 +50,17 @@ fun HomeScreen( title = resources().getString(R.string.home), drawerActions = drawerActions, navigationBarActions = navigationBarActions, - // TODO barAction = { FriendsAction() } + barAction = { FriendsAction(onViewFriendsClick) } ) { Feed(feedUiState, continueTask, onEmptyFeedHelp) } } @Composable -fun FriendsAction() { - IconButton(onClick = { /*TODO*/ }) { +fun FriendsAction( + onClick: () -> Unit +) { + IconButton(onClick = onClick) { Icon( imageVector = Icons.Default.Person, contentDescription = resources().getString(R.string.friends) @@ -69,9 +72,9 @@ fun FriendsAction() { @Composable fun HomeScreenPreview() { HomeScreen( + onViewFriendsClick = {}, drawerActions = DrawerActions({}, {}, {}, {}, {}), navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), - open = {}, feedUiState = FeedUiState.Succes( mapOf( "08 May 2023" to listOf( diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt new file mode 100644 index 0000000..5a9407a --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt @@ -0,0 +1,17 @@ +package be.ugent.sel.studeez.screens.home +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + logService: LogService +) : StudeezViewModel(logService) { + + + fun onViewFriendsClick(open: (String) -> Unit) { + open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) + } +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt deleted file mode 100644 index 9ecaba3..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package be.ugent.sel.studeez.screens.profile - -data class ProfileEditUiState ( - val username: String = "" -) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt index 9c76337..ca59fba 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt @@ -1,37 +1,50 @@ package be.ugent.sel.studeez.screens.profile +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.composable.Headline import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate import be.ugent.sel.studeez.common.composable.drawer.DrawerActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions +import be.ugent.sel.studeez.common.ext.defaultButtonShape import be.ugent.sel.studeez.resources +import be.ugent.sel.studeez.ui.theme.StudeezTheme import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import be.ugent.sel.studeez.R.string as AppText data class ProfileActions( val getUsername: suspend CoroutineScope.() -> String?, + val getBiography: suspend CoroutineScope.() -> String?, + val getAmountOfFriends: () -> Flow, val onEditProfileClick: () -> Unit, + val onViewFriendsClick: () -> Unit ) fun getProfileActions( viewModel: ProfileViewModel, - open: (String) -> Unit, + open: (String) -> Unit ): ProfileActions { return ProfileActions( getUsername = { viewModel.getUsername() }, + getBiography = { viewModel.getBiography() }, + getAmountOfFriends = { viewModel.getAmountOfFriends() }, onEditProfileClick = { viewModel.onEditProfileClick(open) }, + onViewFriendsClick = { viewModel.onViewFriendsClick(open) } ) } @@ -56,8 +69,12 @@ fun ProfileScreen( navigationBarActions: NavigationBarActions, ) { var username: String? by remember { mutableStateOf("") } + var biography: String? by remember { mutableStateOf("") } + val amountOfFriends = profileActions.getAmountOfFriends().collectAsState(initial = 0) + LaunchedEffect(key1 = Unit) { username = profileActions.getUsername(this) + biography = profileActions.getBiography(this) } PrimaryScreenTemplate( title = resources().getString(AppText.profile), @@ -65,7 +82,35 @@ fun ProfileScreen( navigationBarActions = navigationBarActions, barAction = { EditAction(onClick = profileActions.onEditProfileClick) } ) { - Headline(text = (username ?: resources().getString(R.string.no_username))) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + item { + Headline(text = username ?: resources().getString(AppText.no_username)) + } + + item { + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier.fillMaxWidth() + .wrapContentWidth(align = Alignment.CenterHorizontally) + ) { + AmountOfFriendsButton( + amountOfFriends = amountOfFriends.value + ) { + profileActions.onViewFriendsClick() + } + } + } + + item { + Text( + text = biography ?: "", + textAlign = TextAlign.Center, + modifier = Modifier.padding(48.dp, 0.dp) + ) + } + } } } @@ -78,7 +123,6 @@ fun EditAction( imageVector = Icons.Default.Edit, contentDescription = resources().getString(AppText.edit_profile) ) - } } @@ -86,8 +130,38 @@ fun EditAction( @Composable fun ProfileScreenPreview() { ProfileScreen( - profileActions = ProfileActions({ null }, {}), + profileActions = ProfileActions({ null }, { null }, { emptyFlow() }, {}, {}), drawerActions = DrawerActions({}, {}, {}, {}, {}), navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) ) +} + +@Composable +fun AmountOfFriendsButton( + amountOfFriends: Int, + onClick: () -> Unit +){ + Button( + onClick = onClick, + shape = defaultButtonShape() + ) { + Text( + text = resources().getQuantityString( + /* id = */ R.plurals.friends_amount, + /* quantity = */ amountOfFriends, + /* ...formatArgs = */ amountOfFriends + ) + ) + } +} + +@Preview +@Composable +fun AmountOfFriendsButtonPreview() { + StudeezTheme { + Column { + AmountOfFriendsButton(amountOfFriends = 1) { } + AmountOfFriendsButton(amountOfFriends = 100) { } + } + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt index e24defd..93fa086 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt @@ -1,24 +1,40 @@ package be.ugent.sel.studeez.screens.profile +import be.ugent.sel.studeez.domain.FriendshipDAO import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.UserDAO import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.screens.StudeezViewModel +import com.google.firebase.auth.FirebaseAuth import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( private val userDAO: UserDAO, + private val friendshipDAO: FriendshipDAO, logService: LogService ) : StudeezViewModel(logService) { - suspend fun getUsername(): String? { - return userDAO.getUsername() + suspend fun getUsername(): String { + return userDAO.getLoggedInUser().username + } + + suspend fun getBiography(): String { + return userDAO.getLoggedInUser().biography + } + + fun getAmountOfFriends(): Flow { + return friendshipDAO.getFriendshipCount(userDAO.getCurrentUserId()) } fun onEditProfileClick(open: (String) -> Unit) { open(StudeezDestinations.EDIT_PROFILE_SCREEN) } + fun onViewFriendsClick(open: (String) -> Unit) { + open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) + } + } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditScreen.kt similarity index 54% rename from app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditScreen.kt index c6fcbaf..31dcb9d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditScreen.kt @@ -1,20 +1,21 @@ -package be.ugent.sel.studeez.screens.profile +package be.ugent.sel.studeez.screens.profile.edit_profile -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.composable.BasicTextButton import be.ugent.sel.studeez.common.composable.LabelledInputField import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate import be.ugent.sel.studeez.common.ext.textButton import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.ui.theme.StudeezTheme +import be.ugent.sel.studeez.R.string as AppText data class EditProfileActions( val onUserNameChange: (String) -> Unit, + val onBiographyChange: (String) -> Unit, val onSaveClick: () -> Unit, val onDeleteClick: () -> Unit ) @@ -25,6 +26,7 @@ fun getEditProfileActions( ): EditProfileActions { return EditProfileActions( onUserNameChange = { viewModel.onUsernameChange(it) }, + onBiographyChange = { viewModel.onBiographyChange(it) }, onSaveClick = { viewModel.onSaveClick() }, onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, ) @@ -51,28 +53,41 @@ fun EditProfileScreen( editProfileActions: EditProfileActions, ) { SecondaryScreenTemplate( - title = resources().getString(R.string.editing_profile), + title = resources().getString(AppText.editing_profile), popUp = goBack ) { - Column { - LabelledInputField( - value = uiState.username, - onNewValue = editProfileActions.onUserNameChange, - label = R.string.username - ) - BasicTextButton( - text = R.string.save, - Modifier.textButton(), - action = { - editProfileActions.onSaveClick() - goBack() - } - ) - BasicTextButton( - text = R.string.delete_profile, - Modifier.textButton(), - action = editProfileActions.onDeleteClick - ) + LazyColumn { + item { + LabelledInputField( + value = uiState.username, + onNewValue = editProfileActions.onUserNameChange, + label = AppText.username + ) + } + item { + LabelledInputField( + value = uiState.biography, + onNewValue = editProfileActions.onBiographyChange, + label = AppText.biography + ) + } + item { + BasicTextButton( + text = AppText.save, + Modifier.textButton(), + action = { + editProfileActions.onSaveClick() + goBack() + } + ) + } + item { + BasicTextButton( + text = AppText.delete_profile, + Modifier.textButton(), + action = editProfileActions.onDeleteClick + ) + } } } } @@ -81,6 +96,6 @@ fun EditProfileScreen( @Composable fun EditProfileScreenComposable() { StudeezTheme { - EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {})) + EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {}, {})) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditUiState.kt new file mode 100644 index 0000000..911df68 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditUiState.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.screens.profile.edit_profile + +data class ProfileEditUiState ( + val username: String = "", + val biography: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditViewModel.kt similarity index 66% rename from app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditViewModel.kt index cb270be..57bbbc0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditViewModel.kt @@ -1,8 +1,9 @@ -package be.ugent.sel.studeez.screens.profile +package be.ugent.sel.studeez.screens.profile.edit_profile import androidx.compose.runtime.mutableStateOf import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.snackbar.SnackbarManager +import be.ugent.sel.studeez.data.local.models.User import be.ugent.sel.studeez.domain.AccountDAO import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.UserDAO @@ -23,7 +24,11 @@ class ProfileEditViewModel @Inject constructor( init { launchCatching { - uiState.value = uiState.value.copy(username = userDAO.getUsername()!!) + val user: User = userDAO.getLoggedInUser() + uiState.value = uiState.value.copy( + username = user.username, + biography = user.biography + ) } } @@ -31,16 +36,23 @@ class ProfileEditViewModel @Inject constructor( uiState.value = uiState.value.copy(username = newValue) } + fun onBiographyChange(newValue: String) { + uiState.value = uiState.value.copy(biography = newValue) + } + fun onSaveClick() { launchCatching { - userDAO.save(uiState.value.username) + userDAO.saveLoggedInUser( + newUsername = uiState.value.username, + newBiography = uiState.value.biography + ) SnackbarManager.showMessage(R.string.success) } } fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { launchCatching { - userDAO.deleteUserReferences() // Delete references + userDAO.deleteLoggedInUserReferences() // Delete references accountDAO.deleteAccount() // Delete authentication } openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileScreen.kt new file mode 100644 index 0000000..41e33c5 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileScreen.kt @@ -0,0 +1,178 @@ +package be.ugent.sel.studeez.screens.profile.public_profile + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MailOutline +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.common.composable.Headline +import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate +import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.resources +import be.ugent.sel.studeez.screens.profile.AmountOfFriendsButton +import be.ugent.sel.studeez.ui.theme.StudeezTheme +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import be.ugent.sel.studeez.R.string as AppText + +data class PublicProfileActions( + val getUserDetails: () -> Flow, + val getAmountOfFriends: () -> Flow, + val onViewFriendsClick: () -> Unit, + val sendFriendRequest: () -> Boolean +) + +fun getPublicProfileActions( + viewModel: PublicProfileViewModel, + open: (String) -> Unit +): PublicProfileActions { + return PublicProfileActions( + getUserDetails = { viewModel.getUserDetails(viewModel.uiState.value.userId) }, + getAmountOfFriends = { viewModel.getAmountOfFriends( + userId = viewModel.uiState.value.userId + ) }, + onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, + sendFriendRequest = { viewModel.sendFriendRequest( + userId = viewModel.uiState.value.userId + ) } + ) +} + +@Composable +fun PublicProfileRoute( + popUp: () -> Unit, + open: (String) -> Unit, + viewModel: PublicProfileViewModel +) { + PublicProfileScreen( + publicProfileActions = getPublicProfileActions( + viewModel = viewModel, + open = open + ), + popUp = popUp + ) +} + +@Composable +fun PublicProfileScreen( + publicProfileActions: PublicProfileActions, + popUp: () -> Unit +) { + val user = publicProfileActions.getUserDetails().collectAsState(initial = User()) + val amountOfFriends = publicProfileActions.getAmountOfFriends().collectAsState(initial = 0) + + SecondaryScreenTemplate( + title = stringResource(id = AppText.profile), + popUp = popUp, + barAction = { + PublicProfileEllipsis( + sendFriendRequest = publicProfileActions.sendFriendRequest + ) + } + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + item { + Headline(text = user.value.username) + } + + item { + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(align = Alignment.CenterHorizontally) + ) { + AmountOfFriendsButton( + amountOfFriends = amountOfFriends.value + ) { + publicProfileActions.onViewFriendsClick() + } + } + } + + item { + Text( + text = user.value.biography, + textAlign = TextAlign.Center, + modifier = Modifier.padding(48.dp, 0.dp) + ) + } + } + } +} + +@Preview +@Composable +fun PublicProfilePreview() { + StudeezTheme { + PublicProfileScreen( + publicProfileActions = PublicProfileActions( + getUserDetails = { + flowOf(User( + id = "someid", + username = "Maxime De Poorter", + biography = "I am a different student and this is my public profile" + )) + }, + getAmountOfFriends = { flowOf(113) }, + onViewFriendsClick = {}, + sendFriendRequest = { true } + ), + popUp = {} + ) + } +} + +@Composable +fun PublicProfileEllipsis( + sendFriendRequest: () -> Boolean +) { + var expanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { expanded = true } + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal), + contentDescription = resources().getString(AppText.view_more), + modifier = Modifier.fillMaxSize() + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem(onClick = { expanded = false }) { + DrawerEntry( + icon = Icons.Default.MailOutline, + text = stringResource(id = AppText.send_friend_request) + ) { + sendFriendRequest() + } + } + } +} + +@Preview +@Composable +fun PublicProfileEllipsisPreview() { + StudeezTheme { + PublicProfileEllipsis( + sendFriendRequest = { true } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileUiState.kt new file mode 100644 index 0000000..537fed9 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileUiState.kt @@ -0,0 +1,5 @@ +package be.ugent.sel.studeez.screens.profile.public_profile + +data class PublicProfileUiState( + var userId: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileViewModel.kt new file mode 100644 index 0000000..031950c --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileViewModel.kt @@ -0,0 +1,60 @@ +package be.ugent.sel.studeez.screens.profile.public_profile + +import androidx.compose.runtime.mutableStateOf +import be.ugent.sel.studeez.data.SelectedUserId +import be.ugent.sel.studeez.data.local.models.User +import be.ugent.sel.studeez.domain.FriendshipDAO +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.domain.UserDAO +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@HiltViewModel +class PublicProfileViewModel @Inject constructor( + private val userDAO: UserDAO, + private val friendshipDAO: FriendshipDAO, + selectedUserIdState: SelectedUserId, + logService: LogService +): StudeezViewModel(logService) { + + val uiState = mutableStateOf( + PublicProfileUiState( + userId = selectedUserIdState.value + ) + ) + + fun getUserDetails( + userId: String + ): Flow { + uiState.value = uiState.value.copy( + userId = userId + ) + return userDAO.getUserDetails( + userId = uiState.value.userId + ) + } + + fun getAmountOfFriends( + userId: String + ): Flow { + return friendshipDAO.getFriendshipCount( + userId = userId + ) + } + + fun onViewFriendsClick( + open: (String) -> Unit + ) { + open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) + } + + fun sendFriendRequest( + userId: String + ): Boolean { + return friendshipDAO.sendFriendshipRequest(userId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt index a08d063..4cfa6a9 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt @@ -66,7 +66,7 @@ class SignUpViewModel @Inject constructor( launchCatching { accountDAO.signUpWithEmailAndPassword(email, password) accountDAO.signInWithEmailAndPassword(email, password) - userDAO.save(username) + userDAO.saveLoggedInUser(username) openAndPopUp(HOME_SCREEN, SIGN_UP_SCREEN) } } diff --git a/app/src/main/res/drawable/ic_more_horizontal.xml b/app/src/main/res/drawable/ic_more_horizontal.xml new file mode 100644 index 0000000..afbe22d --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horizontal.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5f350e..f7d2666 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,11 +16,12 @@ Go back Next Start + View more Success! Try again - Something wrong happened. Please try again. + Something went wrong. Please try again. Please insert a valid email. @@ -61,6 +62,7 @@ Edit profile Editing profile Delete profile + Bio @@ -120,7 +122,16 @@ Friends Friend + + %d Friend + %d Friends + Adding friends still needs to be implemented. Hang on tight! + You don\'t have any friends yet. Add one! + Search friends + Send friend request + Remove as friend + Show profile