Merge remote-tracking branch 'origin/development' into uitesting
This commit is contained in:
		
						commit
						4244770f95
					
				
					 43 changed files with 1725 additions and 127 deletions
				
			
		|  | @ -33,7 +33,11 @@ import be.ugent.sel.studeez.common.ext.defaultButtonShape | ||||||
| import be.ugent.sel.studeez.R.string as AppText | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
| fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { | fun BasicTextButton( | ||||||
|  |     @StringRes text: Int, | ||||||
|  |     modifier: Modifier, | ||||||
|  |     action: () -> Unit | ||||||
|  | ) { | ||||||
|     TextButton( |     TextButton( | ||||||
|         onClick = action, |         onClick = action, | ||||||
|         modifier = modifier |         modifier = modifier | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package be.ugent.sel.studeez.common.composable | package be.ugent.sel.studeez.common.composable | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.StringRes | import androidx.annotation.StringRes | ||||||
|  | import androidx.compose.foundation.background | ||||||
| import androidx.compose.foundation.layout.Column | import androidx.compose.foundation.layout.Column | ||||||
| import androidx.compose.foundation.layout.padding | import androidx.compose.foundation.layout.padding | ||||||
| import androidx.compose.foundation.text.KeyboardOptions | 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.Email | ||||||
| import androidx.compose.material.icons.filled.Lock | import androidx.compose.material.icons.filled.Lock | ||||||
| import androidx.compose.material.icons.filled.Person | import androidx.compose.material.icons.filled.Person | ||||||
|  | import androidx.compose.material.icons.filled.Search | ||||||
| import androidx.compose.runtime.* | import androidx.compose.runtime.* | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.res.painterResource | import androidx.compose.ui.res.painterResource | ||||||
|  | @ -21,6 +23,8 @@ import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | import be.ugent.sel.studeez.common.ext.fieldModifier | ||||||
| import be.ugent.sel.studeez.resources | 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.drawable as AppIcon | ||||||
| import be.ugent.sel.studeez.R.string as AppText | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
|  | @ -216,3 +220,33 @@ private fun PasswordField( | ||||||
|         visualTransformation = visualTransformation |         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 | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | @ -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.task.Task | ||||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | 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.data.local.models.timer_info.TimerInfo | ||||||
|  | import be.ugent.sel.studeez.domain.UserDAO | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import javax.inject.Singleton | import javax.inject.Singleton | ||||||
| 
 | 
 | ||||||
|  | @ -43,3 +44,10 @@ class SelectedSubject @Inject constructor() : SelectedState<Subject>() { | ||||||
| class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() { | class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() { | ||||||
|     override lateinit var value: TimerInfo |     override lateinit var value: TimerInfo | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | @Singleton | ||||||
|  | class SelectedUserId @Inject constructor( | ||||||
|  |     userDAO: UserDAO | ||||||
|  | ): SelectedState<String>() { | ||||||
|  |     override var value: String = userDAO.getCurrentUserId() | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  | ) | ||||||
|  | @ -1,3 +1,9 @@ | ||||||
| package be.ugent.sel.studeez.data.local.models | 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 = "" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|  | } | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | package be.ugent.sel.studeez.data.remote | ||||||
|  | 
 | ||||||
|  | object FirebaseSessionReport { | ||||||
|  |     const val STUDYTIME: String = "studyTime" | ||||||
|  |     const val ENDTIME: String = "endTime" | ||||||
|  | } | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | package be.ugent.sel.studeez.data.remote | ||||||
|  | 
 | ||||||
|  | object FirebaseUser { | ||||||
|  |     const val USERNAME: String = "username" | ||||||
|  |     const val BIOGRAPHY: String = "biography" | ||||||
|  | } | ||||||
|  | @ -16,6 +16,9 @@ abstract class DatabaseModule { | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO |     abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO | ||||||
| 
 | 
 | ||||||
|  |     @Binds | ||||||
|  |     abstract fun provideFriendshipDAO(impl: FirebaseFriendshipDAO): FriendshipDAO | ||||||
|  | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO |     abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO | ||||||
| 
 | 
 | ||||||
|  | @ -26,13 +29,13 @@ abstract class DatabaseModule { | ||||||
|     abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService |     abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService | ||||||
| 
 | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO |     abstract fun provideSessionDAO(impl: FirebaseSessionDAO): SessionDAO | ||||||
| 
 | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO |     abstract fun provideSubjectDAO(impl: FirebaseSubjectDAO): SubjectDAO | ||||||
| 
 | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO |     abstract fun provideTaskDAO(impl: FirebaseTaskDAO): TaskDAO | ||||||
| 
 | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO |     abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO | ||||||
|  |  | ||||||
|  | @ -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<List<Friendship>> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @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<Int> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @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 | ||||||
|  | } | ||||||
|  | @ -1,12 +1,19 @@ | ||||||
| package be.ugent.sel.studeez.domain | package be.ugent.sel.studeez.domain | ||||||
| 
 | 
 | ||||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | 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.local.models.timer_info.TimerInfo | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| 
 | 
 | ||||||
| interface SessionDAO { | interface SessionDAO { | ||||||
| 
 | 
 | ||||||
|     fun getSessions(): Flow<List<SessionReport>> |     fun getSessions(): Flow<List<SessionReport>> | ||||||
|  |     suspend fun getSessionsOfUser(userId: String): List<SessionReport> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return a list of pairs, containing the username and all the studysessions of that user. | ||||||
|  |      */ | ||||||
|  |     fun getFriendsSessions(): Flow<List<Pair<String,List<SessionReport>>>> | ||||||
| 
 | 
 | ||||||
|     fun saveSession(newSessionReport: SessionReport) |     fun saveSession(newSessionReport: SessionReport) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,52 @@ | ||||||
| package be.ugent.sel.studeez.domain | package be.ugent.sel.studeez.domain | ||||||
| 
 | 
 | ||||||
|  | import be.ugent.sel.studeez.data.local.models.User | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | 
 | ||||||
| interface UserDAO { | interface UserDAO { | ||||||
| 
 | 
 | ||||||
|     suspend fun getUsername(): String? |     fun getCurrentUserId(): String | ||||||
|     suspend fun save(newUsername: String) |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Delete all references to this user in the database. Similar to the deleteCascade in |      * @return all users | ||||||
|  |      */ | ||||||
|  |     fun getAllUsers(): Flow<List<User>> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return all users based on a query, a trimmed down version of getAllUsers() | ||||||
|  |      */ | ||||||
|  |     fun getUsersWithQuery( | ||||||
|  |         fieldName: String, | ||||||
|  |         value: String | ||||||
|  |     ): Flow<List<User>> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Request information about a user | ||||||
|  |      */ | ||||||
|  |     fun getUserDetails( | ||||||
|  |         userId: String | ||||||
|  |     ): Flow<User> | ||||||
|  | 
 | ||||||
|  |     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<User>, 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. |      * relational databases. | ||||||
|      */ |      */ | ||||||
|     suspend fun deleteUserReferences() |     suspend fun deleteLoggedInUserReferences() | ||||||
|  |     // TODO Should be refactored to fun deleteLoggedInUserReferences(): Boolean, without suspend. | ||||||
| } | } | ||||||
|  | @ -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<List<SessionReport>> { |  | ||||||
|         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) |  | ||||||
| } |  | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| package be.ugent.sel.studeez.domain.implementation | package be.ugent.sel.studeez.domain.implementation | ||||||
| 
 | 
 | ||||||
| object FireBaseCollections { | object FirebaseCollections { | ||||||
|     const val SESSION_COLLECTION = "sessions" |     const val SESSION_COLLECTION = "sessions" | ||||||
|     const val USER_COLLECTION = "users" |     const val USER_COLLECTION = "users" | ||||||
|  |     const val FRIENDS_COLLECTION = "friends" | ||||||
|     const val TIMER_COLLECTION = "timers" |     const val TIMER_COLLECTION = "timers" | ||||||
|     const val SUBJECT_COLLECTION = "subjects" |     const val SUBJECT_COLLECTION = "subjects" | ||||||
|     const val TASK_COLLECTION = "tasks" |     const val TASK_COLLECTION = "tasks" | ||||||
|  | @ -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<List<Friendship>> { | ||||||
|  |         return firestore | ||||||
|  |             .collection(USER_COLLECTION) | ||||||
|  |             .document(userId) | ||||||
|  |             .collection(FRIENDS_COLLECTION) | ||||||
|  |             .snapshots() | ||||||
|  |             .map { it.toObjects(Friendship::class.java) } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getFriendshipCount( | ||||||
|  |         userId: String | ||||||
|  |     ): Flow<Int> { | ||||||
|  |         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 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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<List<SessionReport>> { | ||||||
|  |         return currentUserSessionsCollection() | ||||||
|  |             .snapshots() | ||||||
|  |             .map { it.toObjects(SessionReport::class.java) } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun getSessionsOfUser(userId: String): List<SessionReport> { | ||||||
|  |         val collection = firestore.collection(USER_COLLECTION) | ||||||
|  |             .document(userId) | ||||||
|  |             .collection(SESSION_COLLECTION) | ||||||
|  |             .get().await() | ||||||
|  |         val list: MutableList<SessionReport> = mutableListOf() | ||||||
|  |         for (document in collection) { | ||||||
|  |             val id = document.id | ||||||
|  |             val studyTime: Int = document.getField<Int>(STUDYTIME)!! | ||||||
|  |             val endTime: Timestamp = document.getField<Timestamp>(ENDTIME)!! | ||||||
|  |             list.add(SessionReport(id, studyTime, endTime)) | ||||||
|  |         } | ||||||
|  |         return list | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getFriendsSessions(): Flow<List<Pair<String, List<SessionReport>>>> { | ||||||
|  |         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) | ||||||
|  | } | ||||||
|  | @ -18,7 +18,7 @@ import kotlinx.coroutines.tasks.await | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import kotlin.collections.count | import kotlin.collections.count | ||||||
| 
 | 
 | ||||||
| class FireBaseSubjectDAO @Inject constructor( | class FirebaseSubjectDAO @Inject constructor( | ||||||
|     private val firestore: FirebaseFirestore, |     private val firestore: FirebaseFirestore, | ||||||
|     private val auth: AccountDAO, |     private val auth: AccountDAO, | ||||||
|     private val taskDAO: TaskDAO, |     private val taskDAO: TaskDAO, | ||||||
|  | @ -49,7 +49,7 @@ class FireBaseSubjectDAO @Inject constructor( | ||||||
|     override suspend fun archiveSubject(subject: Subject) { |     override suspend fun archiveSubject(subject: Subject) { | ||||||
|         currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true) |         currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true) | ||||||
|         currentUserSubjectsCollection().document(subject.id) |         currentUserSubjectsCollection().document(subject.id) | ||||||
|             .collection(FireBaseCollections.TASK_COLLECTION) |             .collection(FirebaseCollections.TASK_COLLECTION) | ||||||
|             .taskNotArchived() |             .taskNotArchived() | ||||||
|             .get().await() |             .get().await() | ||||||
|             .documents |             .documents | ||||||
|  | @ -74,16 +74,16 @@ class FireBaseSubjectDAO @Inject constructor( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun currentUserSubjectsCollection(): CollectionReference = |     private fun currentUserSubjectsCollection(): CollectionReference = | ||||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) |         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||||
|             .document(auth.currentUserId) |             .document(auth.currentUserId) | ||||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) |             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||||
| 
 | 
 | ||||||
|     private fun subjectTasksCollection(subject: Subject): CollectionReference = |     private fun subjectTasksCollection(subject: Subject): CollectionReference = | ||||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) |         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||||
|             .document(auth.currentUserId) |             .document(auth.currentUserId) | ||||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) |             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||||
|             .document(subject.id) |             .document(subject.id) | ||||||
|             .collection(FireBaseCollections.TASK_COLLECTION) |             .collection(FirebaseCollections.TASK_COLLECTION) | ||||||
| 
 | 
 | ||||||
|     fun CollectionReference.subjectNotArchived(): Query = |     fun CollectionReference.subjectNotArchived(): Query = | ||||||
|         this.whereEqualTo(SubjectDocument.archived, false) |         this.whereEqualTo(SubjectDocument.archived, false) | ||||||
|  | @ -15,7 +15,7 @@ import kotlinx.coroutines.flow.map | ||||||
| import kotlinx.coroutines.tasks.await | import kotlinx.coroutines.tasks.await | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| class FireBaseTaskDAO @Inject constructor( | class FirebaseTaskDAO @Inject constructor( | ||||||
|     private val firestore: FirebaseFirestore, |     private val firestore: FirebaseFirestore, | ||||||
|     private val auth: AccountDAO, |     private val auth: AccountDAO, | ||||||
| ) : TaskDAO { | ) : TaskDAO { | ||||||
|  | @ -45,12 +45,11 @@ class FireBaseTaskDAO @Inject constructor( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference = |     private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference = | ||||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) |         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||||
|             .document(auth.currentUserId) |             .document(auth.currentUserId) | ||||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) |             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||||
|             .document(subjectId) |             .document(subjectId) | ||||||
|             .collection(FireBaseCollections.TASK_COLLECTION) |             .collection(FirebaseCollections.TASK_COLLECTION) | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Extend CollectionReference and Query with some filters | // Extend CollectionReference and Query with some filters | ||||||
|  | @ -48,8 +48,8 @@ class FirebaseTimerDAO @Inject constructor( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun currentUserTimersCollection(): CollectionReference = |     private fun currentUserTimersCollection(): CollectionReference = | ||||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) |         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||||
|             .document(auth.currentUserId) |             .document(auth.currentUserId) | ||||||
|             .collection(FireBaseCollections.TIMER_COLLECTION) |             .collection(FirebaseCollections.TIMER_COLLECTION) | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -2,34 +2,91 @@ package be.ugent.sel.studeez.domain.implementation | ||||||
| 
 | 
 | ||||||
| import be.ugent.sel.studeez.R | import be.ugent.sel.studeez.R | ||||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | 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.AccountDAO | ||||||
| import be.ugent.sel.studeez.domain.UserDAO | 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.DocumentReference | ||||||
| import com.google.firebase.firestore.FirebaseFirestore | 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 kotlinx.coroutines.tasks.await | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| class FirebaseUserDAO @Inject constructor( | class FirebaseUserDAO @Inject constructor( | ||||||
|     private val firestore: FirebaseFirestore, |     private val firestore: FirebaseFirestore, | ||||||
|     private val auth: AccountDAO |     private val auth: AccountDAO | ||||||
|     ) : UserDAO { | ) : UserDAO { | ||||||
| 
 | 
 | ||||||
|     override suspend fun getUsername(): String? { |     override fun getCurrentUserId(): String { | ||||||
|         return currentUserDocument().get().await().getString("username") |         return auth.currentUserId | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override suspend fun save(newUsername: String) { |  | ||||||
|         currentUserDocument().set(mapOf("username" to newUsername)) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun currentUserDocument(): DocumentReference = |     private fun currentUserDocument(): DocumentReference = | ||||||
|         firestore.collection(USER_COLLECTION).document(auth.currentUserId) |         firestore | ||||||
|  |             .collection(USER_COLLECTION) | ||||||
|  |             .document(auth.currentUserId) | ||||||
| 
 | 
 | ||||||
|     companion object { |     override fun getAllUsers(): Flow<List<User>> { | ||||||
|         private const val USER_COLLECTION = "users" |         return firestore | ||||||
|  |             .collection(USER_COLLECTION) | ||||||
|  |             .snapshots() | ||||||
|  |             .map { it.toObjects(User::class.java) } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun deleteUserReferences() { |     override fun getUsersWithQuery( | ||||||
|  |         fieldName: String, | ||||||
|  |         value: String | ||||||
|  |     ): Flow<List<User>> { | ||||||
|  |         return firestore | ||||||
|  |             .collection(USER_COLLECTION) | ||||||
|  |             .whereEqualTo(fieldName, value) | ||||||
|  |             .snapshots() | ||||||
|  |             .map { it.toObjects(User::class.java) } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getUserDetails(userId: String): Flow<User> { | ||||||
|  |         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() |         currentUserDocument().delete() | ||||||
|             .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } |             .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } | ||||||
|             .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } |             .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } | ||||||
|  |  | ||||||
|  | @ -30,7 +30,9 @@ object StudeezDestinations { | ||||||
|     const val EDIT_TASK_FORM = "edit_task" |     const val EDIT_TASK_FORM = "edit_task" | ||||||
| 
 | 
 | ||||||
|     // Friends flow |     // Friends flow | ||||||
|  |     const val FRIENDS_OVERVIEW_SCREEN = "friends_overview" | ||||||
|     const val SEARCH_FRIENDS_SCREEN = "search_friends" |     const val SEARCH_FRIENDS_SCREEN = "search_friends" | ||||||
|  |     const val PUBLIC_PROFILE_SCREEN = "public_profile" | ||||||
| 
 | 
 | ||||||
|     // Create & edit screens |     // Create & edit screens | ||||||
|     const val CREATE_TASK_SCREEN = "create_task" |     const val CREATE_TASK_SCREEN = "create_task" | ||||||
|  |  | ||||||
|  | @ -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.NavigationBarActions | ||||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | 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.home.HomeRoute | ||||||
| import be.ugent.sel.studeez.screens.log_in.LoginRoute | 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.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.SessionRoute | ||||||
| import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute | import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute | ||||||
| import be.ugent.sel.studeez.screens.sessions.SessionsRoute | import be.ugent.sel.studeez.screens.sessions.SessionsRoute | ||||||
|  | @ -69,6 +72,7 @@ fun StudeezNavGraph( | ||||||
|                 drawerActions = drawerActions, |                 drawerActions = drawerActions, | ||||||
|                 navigationBarActions = navigationBarActions, |                 navigationBarActions = navigationBarActions, | ||||||
|                 feedViewModel = hiltViewModel(), |                 feedViewModel = hiltViewModel(), | ||||||
|  |                 viewModel = hiltViewModel() | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -221,8 +225,28 @@ fun StudeezNavGraph( | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Friends flow |         // Friends flow | ||||||
|  |         composable(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) { | ||||||
|  |             FriendsOveriewRoute( | ||||||
|  |                 open = open, | ||||||
|  |                 popUp = goBack, | ||||||
|  |                 viewModel = hiltViewModel() | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) { |         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 |         // Create & edit screens | ||||||
|  |  | ||||||
|  | @ -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<List<Pair<User, Friendship>>>, | ||||||
|  |     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 = { } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | package be.ugent.sel.studeez.screens.friends.friends_overview | ||||||
|  | 
 | ||||||
|  | data class FriendsOverviewUiState( | ||||||
|  |     val userId: String, | ||||||
|  |     val queryString: String = "" | ||||||
|  | ) | ||||||
|  | @ -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<List<Pair<User, Friendship>>> { | ||||||
|  |         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 | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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<List<User>> = emptyFlow() | ||||||
|  | ) | ||||||
|  | @ -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<List<User>>, | ||||||
|  |     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 = { } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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<List<User>> { | ||||||
|  |         return userDAO.getUsersWithQuery( | ||||||
|  |             fieldName = FirebaseUser.USERNAME, | ||||||
|  |             value = value | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get all users, except for the current user. | ||||||
|  |      */ | ||||||
|  |     fun getAllUsers(): Flow<List<User>> { | ||||||
|  |         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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -21,14 +21,15 @@ import be.ugent.sel.studeez.resources | ||||||
| @Composable | @Composable | ||||||
| fun HomeRoute( | fun HomeRoute( | ||||||
|     open: (String) -> Unit, |     open: (String) -> Unit, | ||||||
|  |     viewModel: HomeViewModel, | ||||||
|     drawerActions: DrawerActions, |     drawerActions: DrawerActions, | ||||||
|     navigationBarActions: NavigationBarActions, |     navigationBarActions: NavigationBarActions, | ||||||
|     feedViewModel: FeedViewModel, |     feedViewModel: FeedViewModel, | ||||||
| ) { | ) { | ||||||
|     val feedUiState by feedViewModel.uiState.collectAsState() |     val feedUiState by feedViewModel.uiState.collectAsState() | ||||||
|     HomeScreen( |     HomeScreen( | ||||||
|  |         onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, | ||||||
|         drawerActions = drawerActions, |         drawerActions = drawerActions, | ||||||
|         open = open, |  | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|         feedUiState = feedUiState, |         feedUiState = feedUiState, | ||||||
|         continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, |         continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, | ||||||
|  | @ -38,7 +39,7 @@ fun HomeRoute( | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
| fun HomeScreen( | fun HomeScreen( | ||||||
|     open: (String) -> Unit, |     onViewFriendsClick: () -> Unit, | ||||||
|     drawerActions: DrawerActions, |     drawerActions: DrawerActions, | ||||||
|     navigationBarActions: NavigationBarActions, |     navigationBarActions: NavigationBarActions, | ||||||
|     feedUiState: FeedUiState, |     feedUiState: FeedUiState, | ||||||
|  | @ -49,15 +50,17 @@ fun HomeScreen( | ||||||
|         title = resources().getString(R.string.home), |         title = resources().getString(R.string.home), | ||||||
|         drawerActions = drawerActions, |         drawerActions = drawerActions, | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|         // TODO barAction = { FriendsAction() } |         barAction = { FriendsAction(onViewFriendsClick) } | ||||||
|     ) { |     ) { | ||||||
|         Feed(feedUiState, continueTask, onEmptyFeedHelp) |         Feed(feedUiState, continueTask, onEmptyFeedHelp) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
| fun FriendsAction() { | fun FriendsAction( | ||||||
|     IconButton(onClick = { /*TODO*/ }) { |     onClick: () -> Unit | ||||||
|  | ) { | ||||||
|  |     IconButton(onClick = onClick) { | ||||||
|         Icon( |         Icon( | ||||||
|             imageVector = Icons.Default.Person, |             imageVector = Icons.Default.Person, | ||||||
|             contentDescription = resources().getString(R.string.friends) |             contentDescription = resources().getString(R.string.friends) | ||||||
|  | @ -69,9 +72,9 @@ fun FriendsAction() { | ||||||
| @Composable | @Composable | ||||||
| fun HomeScreenPreview() { | fun HomeScreenPreview() { | ||||||
|     HomeScreen( |     HomeScreen( | ||||||
|  |         onViewFriendsClick = {}, | ||||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), |         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), |         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), | ||||||
|         open = {}, |  | ||||||
|         feedUiState = FeedUiState.Succes( |         feedUiState = FeedUiState.Succes( | ||||||
|             mapOf( |             mapOf( | ||||||
|                 "08 May 2023" to listOf( |                 "08 May 2023" to listOf( | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| package be.ugent.sel.studeez.screens.profile |  | ||||||
| 
 |  | ||||||
| data class ProfileEditUiState ( |  | ||||||
|     val username: String = "" |  | ||||||
| ) |  | ||||||
|  | @ -1,37 +1,50 @@ | ||||||
| package be.ugent.sel.studeez.screens.profile | 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.Icon | ||||||
| import androidx.compose.material.IconButton | import androidx.compose.material.IconButton | ||||||
|  | import androidx.compose.material.Text | ||||||
| import androidx.compose.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.Edit | import androidx.compose.material.icons.filled.Edit | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.* | ||||||
| import androidx.compose.runtime.LaunchedEffect | import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.runtime.getValue | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.runtime.mutableStateOf | import androidx.compose.ui.text.style.TextAlign | ||||||
| import androidx.compose.runtime.remember |  | ||||||
| import androidx.compose.runtime.setValue |  | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
| import be.ugent.sel.studeez.R | import be.ugent.sel.studeez.R | ||||||
| import be.ugent.sel.studeez.common.composable.Headline | import be.ugent.sel.studeez.common.composable.Headline | ||||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | 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.resources | ||||||
|  | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.emptyFlow | ||||||
| import be.ugent.sel.studeez.R.string as AppText | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
| data class ProfileActions( | data class ProfileActions( | ||||||
|     val getUsername: suspend CoroutineScope.() -> String?, |     val getUsername: suspend CoroutineScope.() -> String?, | ||||||
|  |     val getBiography: suspend CoroutineScope.() -> String?, | ||||||
|  |     val getAmountOfFriends: () -> Flow<Int>, | ||||||
|     val onEditProfileClick: () -> Unit, |     val onEditProfileClick: () -> Unit, | ||||||
|  |     val onViewFriendsClick: () -> Unit | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| fun getProfileActions( | fun getProfileActions( | ||||||
|     viewModel: ProfileViewModel, |     viewModel: ProfileViewModel, | ||||||
|     open: (String) -> Unit, |     open: (String) -> Unit | ||||||
| ): ProfileActions { | ): ProfileActions { | ||||||
|     return ProfileActions( |     return ProfileActions( | ||||||
|         getUsername = { viewModel.getUsername() }, |         getUsername = { viewModel.getUsername() }, | ||||||
|  |         getBiography = { viewModel.getBiography() }, | ||||||
|  |         getAmountOfFriends = { viewModel.getAmountOfFriends() }, | ||||||
|         onEditProfileClick = { viewModel.onEditProfileClick(open) }, |         onEditProfileClick = { viewModel.onEditProfileClick(open) }, | ||||||
|  |         onViewFriendsClick = { viewModel.onViewFriendsClick(open) } | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -56,8 +69,12 @@ fun ProfileScreen( | ||||||
|     navigationBarActions: NavigationBarActions, |     navigationBarActions: NavigationBarActions, | ||||||
| ) { | ) { | ||||||
|     var username: String? by remember { mutableStateOf("") } |     var username: String? by remember { mutableStateOf("") } | ||||||
|  |     var biography: String? by remember { mutableStateOf("") } | ||||||
|  |     val amountOfFriends = profileActions.getAmountOfFriends().collectAsState(initial = 0) | ||||||
|  | 
 | ||||||
|     LaunchedEffect(key1 = Unit) { |     LaunchedEffect(key1 = Unit) { | ||||||
|         username = profileActions.getUsername(this) |         username = profileActions.getUsername(this) | ||||||
|  |         biography = profileActions.getBiography(this) | ||||||
|     } |     } | ||||||
|     PrimaryScreenTemplate( |     PrimaryScreenTemplate( | ||||||
|         title = resources().getString(AppText.profile), |         title = resources().getString(AppText.profile), | ||||||
|  | @ -65,7 +82,35 @@ fun ProfileScreen( | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|         barAction = { EditAction(onClick = profileActions.onEditProfileClick) } |         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, |             imageVector = Icons.Default.Edit, | ||||||
|             contentDescription = resources().getString(AppText.edit_profile) |             contentDescription = resources().getString(AppText.edit_profile) | ||||||
|         ) |         ) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -86,8 +130,38 @@ fun EditAction( | ||||||
| @Composable | @Composable | ||||||
| fun ProfileScreenPreview() { | fun ProfileScreenPreview() { | ||||||
|     ProfileScreen( |     ProfileScreen( | ||||||
|         profileActions = ProfileActions({ null }, {}), |         profileActions = ProfileActions({ null }, { null }, { emptyFlow() }, {}, {}), | ||||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), |         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) |         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) { } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,24 +1,40 @@ | ||||||
| package be.ugent.sel.studeez.screens.profile | 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.LogService | ||||||
| import be.ugent.sel.studeez.domain.UserDAO | import be.ugent.sel.studeez.domain.UserDAO | ||||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | import be.ugent.sel.studeez.screens.StudeezViewModel | ||||||
|  | import com.google.firebase.auth.FirebaseAuth | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class ProfileViewModel @Inject constructor( | class ProfileViewModel @Inject constructor( | ||||||
|     private val userDAO: UserDAO, |     private val userDAO: UserDAO, | ||||||
|  |     private val friendshipDAO: FriendshipDAO, | ||||||
|     logService: LogService |     logService: LogService | ||||||
| ) : StudeezViewModel(logService) { | ) : StudeezViewModel(logService) { | ||||||
| 
 | 
 | ||||||
|     suspend fun getUsername(): String? { |     suspend fun getUsername(): String { | ||||||
|         return userDAO.getUsername() |         return userDAO.getLoggedInUser().username | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suspend fun getBiography(): String { | ||||||
|  |         return userDAO.getLoggedInUser().biography | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun getAmountOfFriends(): Flow<Int> { | ||||||
|  |         return friendshipDAO.getFriendshipCount(userDAO.getCurrentUserId()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onEditProfileClick(open: (String) -> Unit) { |     fun onEditProfileClick(open: (String) -> Unit) { | ||||||
|         open(StudeezDestinations.EDIT_PROFILE_SCREEN) |         open(StudeezDestinations.EDIT_PROFILE_SCREEN) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun onViewFriendsClick(open: (String) -> Unit) { | ||||||
|  |         open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -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.Composable | ||||||
| import androidx.compose.runtime.getValue | import androidx.compose.runtime.getValue | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.tooling.preview.Preview | 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.BasicTextButton | ||||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||||
| import be.ugent.sel.studeez.common.ext.textButton | import be.ugent.sel.studeez.common.ext.textButton | ||||||
| import be.ugent.sel.studeez.resources | import be.ugent.sel.studeez.resources | ||||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
|  | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
| data class EditProfileActions( | data class EditProfileActions( | ||||||
|     val onUserNameChange: (String) -> Unit, |     val onUserNameChange: (String) -> Unit, | ||||||
|  |     val onBiographyChange: (String) -> Unit, | ||||||
|     val onSaveClick: () -> Unit, |     val onSaveClick: () -> Unit, | ||||||
|     val onDeleteClick: () -> Unit |     val onDeleteClick: () -> Unit | ||||||
| ) | ) | ||||||
|  | @ -25,6 +26,7 @@ fun getEditProfileActions( | ||||||
| ): EditProfileActions { | ): EditProfileActions { | ||||||
|     return EditProfileActions( |     return EditProfileActions( | ||||||
|         onUserNameChange = { viewModel.onUsernameChange(it) }, |         onUserNameChange = { viewModel.onUsernameChange(it) }, | ||||||
|  |         onBiographyChange = { viewModel.onBiographyChange(it) }, | ||||||
|         onSaveClick = { viewModel.onSaveClick() }, |         onSaveClick = { viewModel.onSaveClick() }, | ||||||
|         onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, |         onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, | ||||||
|     ) |     ) | ||||||
|  | @ -51,36 +53,49 @@ fun EditProfileScreen( | ||||||
|     editProfileActions: EditProfileActions, |     editProfileActions: EditProfileActions, | ||||||
| ) { | ) { | ||||||
|     SecondaryScreenTemplate( |     SecondaryScreenTemplate( | ||||||
|         title = resources().getString(R.string.editing_profile), |         title = resources().getString(AppText.editing_profile), | ||||||
|         popUp = goBack |         popUp = goBack | ||||||
|     ) { |     ) { | ||||||
|         Column { |         LazyColumn { | ||||||
|  |             item { | ||||||
|                 LabelledInputField( |                 LabelledInputField( | ||||||
|                     value = uiState.username, |                     value = uiState.username, | ||||||
|                     onNewValue = editProfileActions.onUserNameChange, |                     onNewValue = editProfileActions.onUserNameChange, | ||||||
|                 label = R.string.username |                     label = AppText.username | ||||||
|                 ) |                 ) | ||||||
|  |             } | ||||||
|  |             item { | ||||||
|  |                 LabelledInputField( | ||||||
|  |                     value = uiState.biography, | ||||||
|  |                     onNewValue = editProfileActions.onBiographyChange, | ||||||
|  |                     label = AppText.biography | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             item { | ||||||
|                 BasicTextButton( |                 BasicTextButton( | ||||||
|                 text = R.string.save, |                     text = AppText.save, | ||||||
|                     Modifier.textButton(), |                     Modifier.textButton(), | ||||||
|                     action = { |                     action = { | ||||||
|                         editProfileActions.onSaveClick() |                         editProfileActions.onSaveClick() | ||||||
|                         goBack() |                         goBack() | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|  |             } | ||||||
|  |             item { | ||||||
|                  BasicTextButton( |                  BasicTextButton( | ||||||
|                 text = R.string.delete_profile, |                     text = AppText.delete_profile, | ||||||
|                     Modifier.textButton(), |                     Modifier.textButton(), | ||||||
|                     action = editProfileActions.onDeleteClick |                     action = editProfileActions.onDeleteClick | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Preview | @Preview | ||||||
| @Composable | @Composable | ||||||
| fun EditProfileScreenComposable() { | fun EditProfileScreenComposable() { | ||||||
|     StudeezTheme { |     StudeezTheme { | ||||||
|         EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {})) |         EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {}, {})) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | package be.ugent.sel.studeez.screens.profile.edit_profile | ||||||
|  | 
 | ||||||
|  | data class ProfileEditUiState ( | ||||||
|  |     val username: String = "", | ||||||
|  |     val biography: String = "" | ||||||
|  | ) | ||||||
|  | @ -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 androidx.compose.runtime.mutableStateOf | ||||||
| import be.ugent.sel.studeez.R | import be.ugent.sel.studeez.R | ||||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | 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.AccountDAO | ||||||
| import be.ugent.sel.studeez.domain.LogService | import be.ugent.sel.studeez.domain.LogService | ||||||
| import be.ugent.sel.studeez.domain.UserDAO | import be.ugent.sel.studeez.domain.UserDAO | ||||||
|  | @ -23,7 +24,11 @@ class ProfileEditViewModel @Inject constructor( | ||||||
| 
 | 
 | ||||||
|     init { |     init { | ||||||
|         launchCatching { |         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) |         uiState.value = uiState.value.copy(username = newValue) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun onBiographyChange(newValue: String) { | ||||||
|  |         uiState.value = uiState.value.copy(biography = newValue) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fun onSaveClick() { |     fun onSaveClick() { | ||||||
|         launchCatching { |         launchCatching { | ||||||
|             userDAO.save(uiState.value.username) |             userDAO.saveLoggedInUser( | ||||||
|  |                 newUsername = uiState.value.username, | ||||||
|  |                 newBiography = uiState.value.biography | ||||||
|  |             ) | ||||||
|             SnackbarManager.showMessage(R.string.success) |             SnackbarManager.showMessage(R.string.success) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { |     fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { | ||||||
|         launchCatching { |         launchCatching { | ||||||
|             userDAO.deleteUserReferences() // Delete references |             userDAO.deleteLoggedInUserReferences() // Delete references | ||||||
|             accountDAO.deleteAccount() // Delete authentication |             accountDAO.deleteAccount() // Delete authentication | ||||||
|         } |         } | ||||||
|         openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) |         openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) | ||||||
|  | @ -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<User>, | ||||||
|  |     val getAmountOfFriends: () -> Flow<Int>, | ||||||
|  |     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 } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | package be.ugent.sel.studeez.screens.profile.public_profile | ||||||
|  | 
 | ||||||
|  | data class PublicProfileUiState( | ||||||
|  |     var userId: String = "" | ||||||
|  | ) | ||||||
|  | @ -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<User> { | ||||||
|  |         uiState.value = uiState.value.copy( | ||||||
|  |             userId = userId | ||||||
|  |         ) | ||||||
|  |         return userDAO.getUserDetails( | ||||||
|  |             userId = uiState.value.userId | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun getAmountOfFriends( | ||||||
|  |         userId: String | ||||||
|  |     ): Flow<Int> { | ||||||
|  |         return friendshipDAO.getFriendshipCount( | ||||||
|  |             userId = userId | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onViewFriendsClick( | ||||||
|  |         open: (String) -> Unit | ||||||
|  |     ) { | ||||||
|  |         open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun sendFriendRequest( | ||||||
|  |         userId: String | ||||||
|  |     ): Boolean { | ||||||
|  |         return friendshipDAO.sendFriendshipRequest(userId) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -66,7 +66,7 @@ class SignUpViewModel @Inject constructor( | ||||||
|         launchCatching { |         launchCatching { | ||||||
|             accountDAO.signUpWithEmailAndPassword(email, password) |             accountDAO.signUpWithEmailAndPassword(email, password) | ||||||
|             accountDAO.signInWithEmailAndPassword(email, password) |             accountDAO.signInWithEmailAndPassword(email, password) | ||||||
|             userDAO.save(username) |             userDAO.saveLoggedInUser(username) | ||||||
|             openAndPopUp(HOME_SCREEN, SIGN_UP_SCREEN) |             openAndPopUp(HOME_SCREEN, SIGN_UP_SCREEN) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_more_horizontal.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_more_horizontal.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | <vector android:height="24dp" android:tint="#000000" | ||||||
|  |     android:viewportHeight="24" android:viewportWidth="24" | ||||||
|  |     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> | ||||||
|  | </vector> | ||||||
|  | @ -16,11 +16,12 @@ | ||||||
|     <string name="go_back">Go back</string> |     <string name="go_back">Go back</string> | ||||||
|     <string name="next">Next</string> |     <string name="next">Next</string> | ||||||
|     <string name="start">Start</string> |     <string name="start">Start</string> | ||||||
|  |     <string name="view_more">View more</string> | ||||||
| 
 | 
 | ||||||
|     <!-- Messages --> |     <!-- Messages --> | ||||||
|     <string name="success">Success!</string> |     <string name="success">Success!</string> | ||||||
|     <string name="try_again">Try again</string> |     <string name="try_again">Try again</string> | ||||||
|     <string name="generic_error">Something wrong happened. Please try again.</string> |     <string name="generic_error">Something went wrong. Please try again.</string> | ||||||
|     <string name="email_error">Please insert a valid email.</string> |     <string name="email_error">Please insert a valid email.</string> | ||||||
| 
 | 
 | ||||||
|     <!-- ========== NavBar ========== --> |     <!-- ========== NavBar ========== --> | ||||||
|  | @ -61,6 +62,7 @@ | ||||||
|     <string name="edit_profile">Edit profile</string> |     <string name="edit_profile">Edit profile</string> | ||||||
|     <string name="editing_profile">Editing profile</string> |     <string name="editing_profile">Editing profile</string> | ||||||
|     <string name="delete_profile">Delete profile</string> |     <string name="delete_profile">Delete profile</string> | ||||||
|  |     <string name="biography">Bio</string> | ||||||
| 
 | 
 | ||||||
|     <!-- ========== Drawer ========== --> |     <!-- ========== Drawer ========== --> | ||||||
| 
 | 
 | ||||||
|  | @ -120,7 +122,16 @@ | ||||||
| 
 | 
 | ||||||
|     <string name="friends">Friends</string> |     <string name="friends">Friends</string> | ||||||
|     <string name="friend">Friend</string> |     <string name="friend">Friend</string> | ||||||
|  |     <plurals name="friends_amount"> | ||||||
|  |         <item quantity="one">%d Friend</item> | ||||||
|  |         <item quantity="other">%d Friends</item> | ||||||
|  |     </plurals> | ||||||
|     <string name="add_friend_not_possible_yet">Adding friends still needs to be implemented. Hang on tight!</string> <!-- TODO Remove this description line once implemented. --> |     <string name="add_friend_not_possible_yet">Adding friends still needs to be implemented. Hang on tight!</string> <!-- TODO Remove this description line once implemented. --> | ||||||
|  |     <string name="no_friends">You don\'t have any friends yet. Add one!</string> | ||||||
|  |     <string name="search_friends">Search friends</string> | ||||||
|  |     <string name="send_friend_request">Send friend request</string> | ||||||
|  |     <string name="remove_friend">Remove as friend</string> | ||||||
|  |     <string name="show_profile">Show profile</string> | ||||||
| 
 | 
 | ||||||
|     <!-- ========== Create & edit screens ========== --> |     <!-- ========== Create & edit screens ========== --> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Rune Dyselinck
						Rune Dyselinck