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
|
||||||
|
|
||||||
|
@ -215,4 +219,34 @@ private fun PasswordField(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
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
|
||||||
|
|
||||||
|
@ -42,4 +43,11 @@ class SelectedSubject @Inject constructor() : SelectedState<Subject>() {
|
||||||
@Singleton
|
@Singleton
|
||||||
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,28 +53,41 @@ 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 {
|
||||||
LabelledInputField(
|
item {
|
||||||
value = uiState.username,
|
LabelledInputField(
|
||||||
onNewValue = editProfileActions.onUserNameChange,
|
value = uiState.username,
|
||||||
label = R.string.username
|
onNewValue = editProfileActions.onUserNameChange,
|
||||||
)
|
label = AppText.username
|
||||||
BasicTextButton(
|
)
|
||||||
text = R.string.save,
|
}
|
||||||
Modifier.textButton(),
|
item {
|
||||||
action = {
|
LabelledInputField(
|
||||||
editProfileActions.onSaveClick()
|
value = uiState.biography,
|
||||||
goBack()
|
onNewValue = editProfileActions.onBiographyChange,
|
||||||
}
|
label = AppText.biography
|
||||||
)
|
)
|
||||||
BasicTextButton(
|
}
|
||||||
text = R.string.delete_profile,
|
item {
|
||||||
Modifier.textButton(),
|
BasicTextButton(
|
||||||
action = editProfileActions.onDeleteClick
|
text = AppText.save,
|
||||||
)
|
Modifier.textButton(),
|
||||||
|
action = {
|
||||||
|
editProfileActions.onSaveClick()
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
BasicTextButton(
|
||||||
|
text = AppText.delete_profile,
|
||||||
|
Modifier.textButton(),
|
||||||
|
action = editProfileActions.onDeleteClick
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +96,6 @@ fun EditProfileScreen(
|
||||||
@Composable
|
@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