Merge branch 'development'

This commit is contained in:
brreynie 2023-05-16 12:02:55 +02:00
commit af6032e75e
73 changed files with 706 additions and 417 deletions

View file

@ -140,7 +140,7 @@ class HomeScreenTest {
fun navigationbarTest() {
var hometest = false
var tasktest = false
var sessiontest = false
var friendstest = false
var profiletest = false
composeTestRule.setContent {
@ -150,7 +150,7 @@ class HomeScreenTest {
{false},
{hometest = true},
{tasktest = true},
{sessiontest = true},
{friendstest = true},
{profiletest = true},
{}, {}, {}
),
@ -183,7 +183,7 @@ class HomeScreenTest {
composeTestRule
.onNodeWithContentDescription(
"session",
"feed",
substring = true,
ignoreCase = true
)
@ -201,7 +201,7 @@ class HomeScreenTest {
Assert.assertTrue(hometest)
Assert.assertTrue(tasktest)
Assert.assertTrue(sessiontest)
Assert.assertTrue(friendstest)
Assert.assertTrue(profiletest)
}
}

View file

@ -2,26 +2,14 @@ package be.ugent.sel.studeez.common.composable
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -87,7 +75,7 @@ fun StealthButton(
) {
//val clickablemodifier = if (disabled) Modifier.clickable(indication = null) else modifier
val borderColor = if (enabled) MaterialTheme.colors.primary
else MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
else MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
BasicButton(
text = text,
onClick = onClick,

View file

@ -29,20 +29,22 @@ fun DrawerScreenTemplate(
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = resources().getString(AppText.menu)
)
}
},
actions = barAction
)},
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = resources().getString(AppText.menu)
)
}
},
actions = barAction
)
},
drawerContent = {
Drawer(drawerActions)
@ -55,10 +57,12 @@ fun DrawerScreenTemplate(
@Preview
@Composable
fun DrawerScreenPreview() {
StudeezTheme { DrawerScreenTemplate(
title = "Drawer screen preview",
drawerActions =DrawerActions({}, {}, {}, {}, {})
) {
Text(text = "Preview content")
} }
StudeezTheme {
DrawerScreenTemplate(
title = "Drawer screen preview",
drawerActions = DrawerActions({}, {}, {}, {}, {})
) {
Text(text = "Preview content")
}
}
}

View file

@ -131,15 +131,19 @@ fun ExpandedEntry(
@Preview
@Composable
fun AddButtonPreview() {
StudeezTheme { AddButton(
addButtonActions = AddButtonActions({}, {}, {})
)}
StudeezTheme {
AddButton(
addButtonActions = AddButtonActions({}, {}, {})
)
}
}
@Preview
@Composable
fun ExpandedAddButtonPreview() {
StudeezTheme { ExpandedAddButton (
addButtonActions = AddButtonActions({}, {}, {})
) }
StudeezTheme {
ExpandedAddButton(
addButtonActions = AddButtonActions({}, {}, {})
)
}
}

View file

@ -57,11 +57,15 @@ fun PrimaryScreenTemplate(
bottomBar = { NavigationBar(navigationBarActions) },
floatingActionButtonPosition = FabPosition.Center,
isFloatingActionButtonDocked = true,
floatingActionButton = { AddButton(AddButtonActions(
onTaskClick = navigationBarActions.onAddTaskClick,
onFriendClick = navigationBarActions.onAddFriendClick,
onSessionClick = navigationBarActions.onAddSessionClick
)) }
floatingActionButton = {
AddButton(
AddButtonActions(
onTaskClick = navigationBarActions.onAddTaskClick,
onFriendClick = navigationBarActions.onAddFriendClick,
onSessionClick = navigationBarActions.onAddSessionClick
)
)
}
) {
content(it)
}

View file

@ -21,18 +21,20 @@ fun SecondaryScreenTemplate(
) {
Scaffold(
// Everything at the top of the screen
topBar = { TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = { popUp() }) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = resources().getString(R.string.go_back)
)
}
},
actions = barAction
) },
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = { popUp() }) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = resources().getString(R.string.go_back)
)
}
},
actions = barAction
)
},
) { paddingValues ->
content(paddingValues)
}
@ -41,8 +43,10 @@ fun SecondaryScreenTemplate(
@Preview
@Composable
fun SecondaryScreenToolbarPreview() {
StudeezTheme { SecondaryScreenTemplate(
"Preview screen",
{}
) {} }
StudeezTheme {
SecondaryScreenTemplate(
"Preview screen",
{}
) {}
}
}

View file

@ -11,6 +11,6 @@ fun SimpleScreenTemplate(
title: String,
content: @Composable (PaddingValues) -> Unit
) {
Scaffold( topBar = { TopAppBar ( title = { Text(text = title) } ) }
Scaffold(topBar = { TopAppBar(title = { Text(text = title) }) }
) { paddingValues -> content(paddingValues) }
}

View file

@ -16,7 +16,7 @@ import androidx.compose.ui.unit.sp
fun Headline(
text: String
) {
Row (
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
@ -32,7 +32,7 @@ fun Headline(
fun DateText(date: String) {
Text(
text = date,
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
modifier = Modifier.padding(horizontal = 10.dp)
)

View file

@ -1,7 +1,6 @@
package be.ugent.sel.studeez.common.composable
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
@ -23,8 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.ext.fieldModifier
import be.ugent.sel.studeez.resources
import com.google.android.material.color.MaterialColors
import kotlin.math.sin
import be.ugent.sel.studeez.R.drawable as AppIcon
import be.ugent.sel.studeez.R.string as AppText
@ -105,7 +102,7 @@ fun LabeledNumberInputField(
singleLine = singleLine,
label = { Text(resources().getString(label)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = {typedInt ->
onValueChange = { typedInt ->
val isNumber = typedInt.matches(Regex("[1-9]+\\d*]"))
if (isNumber) {
number = typedInt.toInt()
@ -164,12 +161,11 @@ fun LabeledErrorTextField(
}
@Preview(showBackground = true)
@Composable
fun IntInputPreview() {
LabeledNumberInputField(value = 1, onNewValue = {}, label = AppText.email)
}
@Preview(showBackground = true)
@Composable
fun IntInputPreview() {
LabeledNumberInputField(value = 1, onNewValue = {}, label = AppText.email)
}
@Composable
fun PasswordField(
@ -227,12 +223,14 @@ fun SearchField(
onValueChange: (String) -> Unit,
onSubmit: () -> Unit,
@StringRes label: Int,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = { Text(text = stringResource(id = label)) },
trailingIcon = {
IconButton(onClick = onSubmit) {

View file

@ -82,7 +82,11 @@ fun TimePickerButton(
}
}
private fun pickDuration(context: Context, onTimeChosen: (Int) -> Unit, timeState: MutableState<Int>) {
private fun pickDuration(
context: Context,
onTimeChosen: (Int) -> Unit,
timeState: MutableState<Int>
) {
val listener = OnTimeSetListener { _, hour, minute ->
timeState.value = HoursMinutesSeconds(hour, minute, 0).getTotalSeconds()
onTimeChosen(timeState.value)

View file

@ -2,12 +2,7 @@ package be.ugent.sel.studeez.common.composable.drawer
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons

View file

@ -3,8 +3,6 @@ package be.ugent.sel.studeez.common.composable.drawer
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations

View file

@ -81,7 +81,7 @@ fun FeedWithElements(
Text(
text = "${HoursMinutesSeconds(totalDayStudyTime)}",
fontSize = 15.sp,
fontWeight = FontWeight.Bold
fontWeight = FontWeight.Medium
)
}
feedEntries.forEach { feedEntry ->

View file

@ -52,11 +52,12 @@ fun FeedEntry(
verticalAlignment = Alignment.CenterVertically,
) {
Column(
verticalArrangement = Arrangement.spacedBy(0.dp)
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.weight(13f)
) {
Text(
text = feedEntry.subJectName,
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Medium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
@ -66,7 +67,10 @@ fun FeedEntry(
maxLines = 1,
)
}
Text(text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString())
Text(
text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString(),
modifier = Modifier.weight(6f),
)
}
}
val buttonText: Int =

View file

@ -8,13 +8,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.outlined.DateRange
import androidx.compose.material.icons.outlined.Face
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme
@ -99,11 +99,11 @@ fun NavigationBar(
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Outlined.DateRange, resources().getString(AppText.sessions)
imageVector = Icons.Outlined.Face, resources().getString(AppText.friends_feed)
)
},
label = { Text(text = resources().getString(AppText.sessions)) },
selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN),
label = { Text(text = resources().getString(AppText.friends_feed)) },
selected = navigationBarActions.isSelectedTab(FRIENDS_FEED),
onClick = navigationBarActions.onSessionsClick
)

View file

@ -2,10 +2,11 @@ package be.ugent.sel.studeez.common.composable.navbar
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SEARCH_FRIENDS_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SELECT_SUBJECT
import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
@ -26,7 +27,7 @@ class NavigationBarViewModel @Inject constructor(
}
fun onSessionsClick(open: (String) -> Unit) {
open(SESSIONS_SCREEN)
open(FRIENDS_FEED)
}
fun onProfileClick(open: (String) -> Unit) {
@ -38,8 +39,7 @@ class NavigationBarViewModel @Inject constructor(
}
fun onAddFriendClick(open: (String) -> Unit) {
// TODO open(SEARCH_FRIENDS_SCREEN)
SnackbarManager.showMessage(AppText.add_friend_not_possible_yet) // TODO Remove
open(SEARCH_FRIENDS_SCREEN)
}
fun onAddSessionClick(open: (String) -> Unit) {

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List
@ -65,16 +66,17 @@ fun SubjectEntry(
) {
Text(
text = subject.name,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontWeight = FontWeight.Medium
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = HoursMinutesSeconds(studytime).toString(),
color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f)
)
Row(
verticalAlignment = Alignment.CenterVertically,
@ -82,9 +84,13 @@ fun SubjectEntry(
) {
Icon(
imageVector = Icons.Default.List,
contentDescription = stringResource(id = AppText.tasks)
contentDescription = stringResource(id = AppText.tasks),
tint = MaterialTheme.colors.onBackground.copy(alpha = 0.6f)
)
Text(
text = "${completedTaskCount}/${taskCount}",
color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f)
)
Text(text = "${completedTaskCount}/${taskCount}")
}
}
}

View file

@ -1,20 +1,20 @@
package be.ugent.sel.studeez.common.ext
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.unit.dp
fun Modifier.textButton(): Modifier {
return this.fillMaxWidth().padding(16.dp, 8.dp, 16.dp, 0.dp)
return this
.fillMaxWidth()
.padding(16.dp, 8.dp, 16.dp, 0.dp)
}
fun Modifier.basicButton(): Modifier {
return this.fillMaxWidth().padding(16.dp, 8.dp)
return this
.fillMaxWidth()
.padding(16.dp, 8.dp)
}
fun Modifier.card(): Modifier {
@ -30,7 +30,9 @@ fun Modifier.dropdownSelector(): Modifier {
}
fun Modifier.fieldModifier(): Modifier {
return this.fillMaxWidth().padding(16.dp, 4.dp)
return this
.fillMaxWidth()
.padding(16.dp, 4.dp)
}
fun Modifier.toolbarActions(): Modifier {
@ -38,9 +40,13 @@ fun Modifier.toolbarActions(): Modifier {
}
fun Modifier.spacer(): Modifier {
return this.fillMaxWidth().padding(12.dp)
return this
.fillMaxWidth()
.padding(12.dp)
}
fun Modifier.smallSpacer(): Modifier {
return this.fillMaxWidth().height(8.dp)
return this
.fillMaxWidth()
.height(8.dp)
}

View file

@ -48,6 +48,6 @@ class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() {
@Singleton
class SelectedUserId @Inject constructor(
userDAO: UserDAO
): SelectedState<String>() {
) : SelectedState<String>() {
override var value: String = userDAO.getCurrentUserId()
}

View file

@ -2,7 +2,6 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
import be.ugent.sel.studeez.data.local.models.SessionReport
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentReference
abstract class FunctionalTimer(initialValue: Int) {
var time: Time = Time(initialValue)

View file

@ -2,7 +2,7 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
data class HoursMinutesSeconds(val hours: Int, val minutes: Int, val seconds: Int) {
constructor(sec: Int): this(
constructor(sec: Int) : this(
hours = sec / (60 * 60),
minutes = (sec / (60)) % 60,
seconds = sec % 60,

View file

@ -8,13 +8,13 @@ class CustomTimerInfo(
description: String,
var studyTime: Int,
id: String = ""
): TimerInfo(id, name, description) {
) : TimerInfo(id, name, description) {
override fun getFunctionalTimer(): FunctionalTimer {
return FunctionalCustomTimer(studyTime)
}
override fun asJson() : Map<String, Any> {
override fun asJson(): Map<String, Any> {
return mapOf(
"type" to "custom",
"name" to name,

View file

@ -7,14 +7,14 @@ class EndlessTimerInfo(
name: String,
description: String,
id: String = ""
): TimerInfo(id, name, description) {
) : TimerInfo(id, name, description) {
override fun getFunctionalTimer(): FunctionalTimer {
return FunctionalEndlessTimer()
}
override fun asJson() : Map<String, Any> {
override fun asJson(): Map<String, Any> {
return mapOf(
"type" to "endless",
"name" to name,

View file

@ -2,7 +2,6 @@ package be.ugent.sel.studeez.data.local.models.timer_info
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
class PomodoroTimerInfo(
name: String,
@ -11,14 +10,14 @@ class PomodoroTimerInfo(
var breakTime: Int,
var repeats: Int,
id: String = ""
): TimerInfo(id, name, description) {
) : TimerInfo(id, name, description) {
override fun getFunctionalTimer(): FunctionalTimer {
return FunctionalPomodoroTimer(studyTime, breakTime, repeats)
}
override fun asJson() : Map<String, Any> {
override fun asJson(): Map<String, Any> {
return mapOf(
"type" to "break",
"name" to name,

View file

@ -7,4 +7,7 @@ interface FeedDAO {
fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>>
suspend fun getFeedEntriesFromUser(id: String): Map<String, List<FeedEntry>>
fun getFriendsSessions(): Flow<Map<String, List<Pair<String, FeedEntry>>>>
}

View file

@ -1,7 +1,6 @@
package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import kotlinx.coroutines.flow.Flow
@ -10,11 +9,6 @@ interface SessionDAO {
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 deleteSession(newTimer: TimerInfo)

View file

@ -20,4 +20,5 @@ interface SubjectDAO {
fun getStudyTime(subject: Subject): Flow<Int>
suspend fun getSubject(subjectId: String): Subject?
suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject
}

View file

@ -15,4 +15,6 @@ interface TaskDAO {
fun deleteTask(oldTask: Task)
suspend fun getTask(subjectId: String, taskId: String): Task
suspend fun getTaskFromUser(subjectId: String, taskId: String, userId: String): Task
}

View file

@ -1,7 +1,6 @@
package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.data.local.models.timer_info.TimerJson
import kotlinx.coroutines.flow.Flow
interface TimerDAO {

View file

@ -1,12 +1,12 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.timer_info.*
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.data.local.models.timer_info.TimerJson
import be.ugent.sel.studeez.domain.ConfigurationService
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.get
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.google.gson.Gson
import kotlinx.coroutines.tasks.await
import javax.inject.Inject

View file

@ -5,19 +5,19 @@ import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.SessionReport
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.domain.FeedDAO
import be.ugent.sel.studeez.domain.SessionDAO
import be.ugent.sel.studeez.domain.SubjectDAO
import be.ugent.sel.studeez.domain.TaskDAO
import be.ugent.sel.studeez.domain.*
import com.google.firebase.Timestamp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class FirebaseFeedDAO @Inject constructor(
private val friendshipDAO: FriendshipDAO,
private val sessionDAO: SessionDAO,
private val taskDAO: TaskDAO,
private val subjectDAO: SubjectDAO
private val subjectDAO: SubjectDAO,
private val auth: AccountDAO,
private val userDAO: UserDAO,
) : FeedDAO {
/**
@ -37,6 +37,45 @@ class FirebaseFeedDAO @Inject constructor(
}
}
/**
* Return a map as with key the day and value a list of feedentries for that day.
*/
override suspend fun getFeedEntriesFromUser(id: String): Map<String, List<FeedEntry>> {
return sessionDAO.getSessionsOfUser(id)
.map { sessionReport -> sessionToFeedEntryFromUser(sessionReport, id) }
.sortedByDescending { it.endTime }
.groupBy { getFormattedTime(it) }
.mapValues { (_, entries) ->
entries
.groupBy { it.taskId }
.map { fuseFeedEntries(it.component2()) }
}
}
override fun getFriendsSessions(): Flow<Map<String, List<Pair<String, FeedEntry>>>> {
return friendshipDAO.getAllFriendships(auth.currentUserId)
.map { friendships ->
friendships.map { friendship ->
val userId: String = friendship.friendId
val username = userDAO.getUsername(userId)
val friendFeed = getFeedEntriesFromUser(userId)
Pair(username, friendFeed)
}
}.map {
mergeNameAndEntries(it)
}
}
private fun mergeNameAndEntries(l: List<Pair<String, Map<String, List<FeedEntry>>>>): Map<String, List<Pair<String, FeedEntry>>> {
val new: MutableMap<String, List<Pair<String, FeedEntry>>> = mutableMapOf()
for ((name, map) in l) {
for ((day, feedEntries: List<FeedEntry>) in map) {
new[day] = new.getOrDefault(day, listOf()) + feedEntries.map { Pair(name, it) }
}
}
return new
}
private fun getFormattedTime(entry: FeedEntry): String {
return DateFormat.getDateInstance().format(entry.endTime.toDate())
}
@ -67,6 +106,14 @@ class FirebaseFeedDAO @Inject constructor(
val task: Task = taskDAO.getTask(subjectId, taskId)
val subject: Subject = subjectDAO.getSubject(subjectId)!!
return makeFeedEntry(sessionReport, subject, task)
}
private fun makeFeedEntry(
sessionReport: SessionReport,
subject: Subject,
task: Task
): FeedEntry {
return FeedEntry(
argb_color = subject.argb_color,
subJectName = subject.name,
@ -78,4 +125,20 @@ class FirebaseFeedDAO @Inject constructor(
isArchived = task.archived || subject.archived
)
}
/**
* Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names
*/
private suspend fun sessionToFeedEntryFromUser(
sessionReport: SessionReport,
id: String
): FeedEntry {
val subjectId: String = sessionReport.subjectId
val taskId: String = sessionReport.taskId
val task: Task = taskDAO.getTaskFromUser(subjectId, taskId, id)
val subject: Subject = subjectDAO.getSubjectOfUSer(subjectId, id)
return makeFeedEntry(sessionReport, subject, task)
}
}

View file

@ -1,11 +1,10 @@
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.data.remote.FirebaseFriendship.FRIENDSSINCE
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.FriendshipDAO
import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.FRIENDS_COLLECTION
@ -27,7 +26,7 @@ import be.ugent.sel.studeez.R.string as AppText
class FirebaseFriendshipDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO
): FriendshipDAO {
) : FriendshipDAO {
private fun currentUserDocument(): DocumentReference = firestore
.collection(USER_COLLECTION)
@ -75,24 +74,44 @@ class FirebaseFriendshipDAO @Inject constructor(
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
// Check if the friendship already exists for the logged in user
var allowed = false
firestore.collection(USER_COLLECTION)
.document(otherUserId)
.document(currentUserId)
.collection(FRIENDS_COLLECTION)
.add(mapOf(
FRIENDID to currentUserId,
ACCEPTED to true, // TODO Make it not automatically accepted.
FRIENDSSINCE to Timestamp.now()
))
.whereEqualTo(FRIENDID, otherUserId)
.get()
.addOnSuccessListener {
allowed = it.documents.isEmpty()
if (allowed) {
// 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()
)
)
}
}.addOnSuccessListener {
val message = if (allowed) AppText.success else AppText.already_friend
SnackbarManager.showMessage(message)
}
return true
}

View file

@ -1,24 +1,15 @@
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
@ -26,8 +17,6 @@ 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>> {
@ -37,32 +26,13 @@ class FirebaseSessionDAO @Inject constructor(
}
override suspend fun getSessionsOfUser(userId: String): List<SessionReport> {
val collection = firestore.collection(USER_COLLECTION)
return 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
.map { it.toObject(SessionReport::class.java) }
}
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)

View file

@ -1,6 +1,5 @@
package be.ugent.sel.studeez.domain.implementation
import android.util.Log
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.SubjectDocument
import be.ugent.sel.studeez.data.local.models.task.Task
@ -35,6 +34,10 @@ class FirebaseSubjectDAO @Inject constructor(
return currentUserSubjectsCollection().document(subjectId).get().await().toObject()
}
override suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject {
return currentUserSubjectsCollection(userId).document(subjectId).get().await().toObject()!!
}
override fun saveSubject(newSubject: Subject) {
currentUserSubjectsCollection().add(newSubject)
}
@ -74,14 +77,17 @@ class FirebaseSubjectDAO @Inject constructor(
.map { tasks -> tasks.sumOf { it.time } }
}
private fun currentUserSubjectsCollection(): CollectionReference =
private fun currentUserSubjectsCollection(id: String = auth.currentUserId): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.document(id)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
private fun subjectTasksCollection(subject: Subject): CollectionReference =
private fun subjectTasksCollection(
subject: Subject,
id: String = auth.currentUserId
): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.document(id)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
.document(subject.id)
.collection(FirebaseCollections.TASK_COLLECTION)

View file

@ -30,6 +30,13 @@ class FirebaseTaskDAO @Inject constructor(
return selectedSubjectTasksCollection(subjectId).document(taskId).get().await().toObject()!!
}
override suspend fun getTaskFromUser(subjectId: String, taskId: String, userId: String): Task {
return selectedSubjectTasksCollection(subjectId, userId)
.document(taskId)
.get()
.await().toObject(Task::class.java)!!
}
override fun saveTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.subjectId).add(newTask)
}
@ -44,9 +51,12 @@ class FirebaseTaskDAO @Inject constructor(
selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete()
}
private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference =
private fun selectedSubjectTasksCollection(
subjectId: String,
id: String = auth.currentUserId
): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.document(id)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
.document(subjectId)
.collection(FirebaseCollections.TASK_COLLECTION)

View file

@ -1,6 +1,7 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.timer_info.*
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.data.local.models.timer_info.TimerJson
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.TimerDAO
import com.google.firebase.firestore.CollectionReference
@ -29,7 +30,7 @@ class FirebaseTimerDAO @Inject constructor(
// Wrap default timers in een flow en combineer met de userTimer flow.
val defaultTimers: List<TimerInfo> = configurationService.getDefaultTimers()
val defaultTimersFlow: Flow<List<TimerInfo>> = flowOf(defaultTimers)
val userTimersFlow: Flow<List<TimerInfo>> = getUserTimers()
val userTimersFlow: Flow<List<TimerInfo>> = getUserTimers()
return defaultTimersFlow.combine(userTimersFlow) { defaultTimersList, userTimersList ->
defaultTimersList + userTimersList
}

View file

@ -80,10 +80,12 @@ class FirebaseUserDAO @Inject constructor(
newUsername: String,
newBiography: String
) {
currentUserDocument().set(mapOf(
USERNAME to newUsername,
BIOGRAPHY to newBiography
))
currentUserDocument().set(
mapOf(
USERNAME to newUsername,
BIOGRAPHY to newBiography
)
)
}
override suspend fun deleteLoggedInUserReferences() {

View file

@ -1,6 +1,7 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.timer_info.*
import be.ugent.sel.studeez.domain.implementation.ToTimerConverter.TimerFactory
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
@ -15,32 +16,38 @@ import com.google.gson.reflect.TypeToken
class ToTimerConverter {
fun interface TimerFactory {
fun makeTimer(map: TimerJson) : TimerInfo
fun makeTimer(map: TimerJson): TimerInfo
}
private val timerInfoMap: Map<TimerType, TimerFactory> = mapOf(
TimerType.ENDLESS to TimerFactory { EndlessTimerInfo(
it.name,
it.description,
it.id
) },
TimerType.CUSTOM to TimerFactory { CustomTimerInfo(
it.name,
it.description,
it.studyTime,
it.id
) },
TimerType.BREAK to TimerFactory { PomodoroTimerInfo(
it.name,
it.description,
it.studyTime,
it.breakTime,
it.repeats,
it.id
) }
TimerType.ENDLESS to TimerFactory {
EndlessTimerInfo(
it.name,
it.description,
it.id
)
},
TimerType.CUSTOM to TimerFactory {
CustomTimerInfo(
it.name,
it.description,
it.studyTime,
it.id
)
},
TimerType.BREAK to TimerFactory {
PomodoroTimerInfo(
it.name,
it.description,
it.studyTime,
it.breakTime,
it.repeats,
it.id
)
}
)
private fun getTimer(timerJson: TimerJson): TimerInfo{
private fun getTimer(timerJson: TimerJson): TimerInfo {
val type: TimerType = TimerType.valueOf(timerJson.type.uppercase())
return timerInfoMap.getValue(type).makeTimer(timerJson)
}

View file

@ -4,7 +4,7 @@ object StudeezDestinations {
// NavBar
const val HOME_SCREEN = "home"
const val SUBJECT_SCREEN = "subjects"
const val SESSIONS_SCREEN = "sessions"
const val FRIENDS_FEED = "friends_feed"
const val PROFILE_SCREEN = "profile"
// Drawer

View file

@ -16,14 +16,14 @@ import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel
import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions
import be.ugent.sel.studeez.screens.friends.friends_overview.FriendsOveriewRoute
import be.ugent.sel.studeez.screens.friends.friends_search.SearchFriendsRoute
import be.ugent.sel.studeez.screens.friends_feed.FriendsFeedRoute
import be.ugent.sel.studeez.screens.home.HomeRoute
import be.ugent.sel.studeez.screens.log_in.LoginRoute
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.edit_profile.EditProfileRoute
import be.ugent.sel.studeez.screens.profile.public_profile.PublicProfileRoute
import be.ugent.sel.studeez.screens.session.SessionRoute
import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute
import be.ugent.sel.studeez.screens.sessions.SessionsRoute
import be.ugent.sel.studeez.screens.settings.SettingsRoute
import be.ugent.sel.studeez.screens.sign_up.SignUpRoute
import be.ugent.sel.studeez.screens.splash.SplashRoute
@ -112,7 +112,12 @@ fun StudeezNavGraph(
composable(StudeezDestinations.TASKS_SCREEN) {
TaskRoute(
goBack = { openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.TASKS_SCREEN) },
goBack = {
openAndPopUp(
StudeezDestinations.SUBJECT_SCREEN,
StudeezDestinations.TASKS_SCREEN
)
},
open = open,
viewModel = hiltViewModel(),
)
@ -135,10 +140,11 @@ fun StudeezNavGraph(
}
composable(StudeezDestinations.SESSIONS_SCREEN) {
SessionsRoute(
composable(StudeezDestinations.FRIENDS_FEED) {
FriendsFeedRoute(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
navigationBarActions = navigationBarActions,
viewModel = hiltViewModel()
)
}

View file

@ -1,15 +1,14 @@
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.material.icons.filled.Search
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -23,7 +22,6 @@ 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
@ -89,13 +87,32 @@ fun FriendsOverviewScreen(
topBar = {
TopAppBar(
title = {
// TODO Link to each other
SearchField(
value = uiState.queryString,
onValueChange = friendsOverviewActions.onQueryStringChange,
onSubmit = friendsOverviewActions.onSubmit,
label = AppText.search_friends
)
// TODO Make search field
// SearchField(
// value = uiState.queryString,
// onValueChange = friendsOverviewActions.onQueryStringChange,
// onSubmit = friendsOverviewActions.onSubmit,
// label = AppText.search_friends,
// enabled = false
// )
IconButton(
onClick = friendsOverviewActions.onSubmit,
// modifier = Modifier.background(
// color = MaterialTheme.colors.background
// ),
) {
Row {
Text(
text = stringResource(id = AppText.click_search_friends),
color = MaterialTheme.colors.onPrimary
)
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(AppText.search_friends),
tint = MaterialTheme.colors.onPrimary
)
}
}
},
navigationIcon = {
IconButton(onClick = popUp) {
@ -109,7 +126,7 @@ fun FriendsOverviewScreen(
)
}
) { paddingValues ->
LazyColumn (
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
if (friends.value.isEmpty()) {
@ -162,49 +179,56 @@ fun FriendsEntry(
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 (
Card {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.spacedBy(15.dp)
) {
Column (
Box(
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
)
ProfilePicture()
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
modifier = Modifier
.fillMaxWidth()
) {
FriendsOverviewDropDown(
friendship = friendship,
viewProfile = viewProfile,
removeFriend = removeFriend
)
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
)
}
}
}
}

View file

@ -23,15 +23,17 @@ class FriendsOverviewViewModel @Inject constructor(
logService: LogService
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(FriendsOverviewUiState(
userId = selectedUserIdState.value
))
var uiState = mutableStateOf(
FriendsOverviewUiState(
userId = selectedUserIdState.value
)
)
private set
fun getAllFriends(): Flow<List<Pair<User, Friendship>>> {
return friendshipDAO.getAllFriendships(
userId = uiState.value.userId
)
userId = uiState.value.userId
)
.flatMapConcat { friendships ->
val userFlows = friendships.map { friendship ->
userDAO.getUserDetails(friendship.friendId)

View file

@ -1,10 +1,8 @@
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
@ -21,7 +19,6 @@ 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
@ -82,14 +79,16 @@ fun SearchFriendsScreen(
topBar = {
TopAppBar(
title = {
SearchField(
value = query,
onValueChange = { newValue ->
searchFriendsActions.onQueryStringChange(newValue)
query = newValue
},
onSubmit = { },
label = AppText.search_friends
// TODO Make search field
// SearchField(
// value = uiState.queryString,
// onValueChange = friendsOverviewActions.onQueryStringChange,
// onSubmit = friendsOverviewActions.onSubmit,
// label = AppText.search_friends,
// enabled = false
// )
Text(
text = stringResource(id = AppText.searching_friends)
)
},
navigationIcon = {
@ -106,7 +105,7 @@ fun SearchFriendsScreen(
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
items (searchResults.value) { user ->
items(searchResults.value) { user ->
UserEntry(
user = user,
goToProfile = searchFriendsActions.goToProfile
@ -124,21 +123,29 @@ fun SearchFriendsPreview() {
popUp = {},
uiState = SearchFriendUiState(
queryString = "dit is een test",
searchResults = flowOf(listOf(User(
id = "someid",
username = "Eerste user",
biography = "blah blah blah"
)))
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"
)))
flowOf(
listOf(
User(
id = "someid",
username = "Eerste user",
biography = "blah blah blah"
)
)
)
},
goToProfile = { }
)
@ -164,11 +171,11 @@ fun UserEntry(
ProfilePicture()
}
Box (
Box(
modifier = Modifier
.fillMaxWidth()
) {
Column (
Column(
modifier = Modifier
.padding(vertical = 4.dp)
) {
@ -179,7 +186,11 @@ fun UserEntry(
overflow = TextOverflow.Ellipsis
)
Text(
text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}",
text = "${resources().getString(AppText.app_name)} ${
resources().getString(
AppText.friend
)
}",
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis

View file

@ -10,7 +10,7 @@ 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 kotlinx.coroutines.flow.map
import javax.inject.Inject
@HiltViewModel
@ -18,7 +18,7 @@ class SearchFriendsViewModel @Inject constructor(
private val userDAO: UserDAO,
private val selectedProfileState: SelectedUserId,
logService: LogService
): StudeezViewModel(logService) {
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(SearchFriendUiState())
private set
@ -49,8 +49,8 @@ class SearchFriendsViewModel @Inject constructor(
*/
fun getAllUsers(): Flow<List<User>> {
return userDAO.getAllUsers()
.filter { users ->
users.any { user ->
.map { users ->
users.filter { user ->
user.id != userDAO.getCurrentUserId()
}
}

View file

@ -0,0 +1,133 @@
package be.ugent.sel.studeez.screens.friends_feed
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.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.composable.DateText
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.feed.LoadingFeed
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun FriendsFeedRoute(
viewModel: FriendsFeedViewModel,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions
) {
val uiState by viewModel.uiState.collectAsState()
FriendsFeedScreen(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
uiState = uiState,
)
}
@Composable
fun FriendsFeedScreen(
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
uiState: FriendsFeedUiState,
) {
PrimaryScreenTemplate(
title = resources().getString(AppText.friends_feed),
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
) {
when (uiState) {
FriendsFeedUiState.Loading -> LoadingFeed()
is FriendsFeedUiState.Succes -> {
val friendsSessions = uiState.friendSessions
LazyColumn {
// Default Timers, cannot be edited
items(friendsSessions) {
val (day, feedEntries) = it
DateText(date = day)
feedEntries.forEach { (name, feedEntry) ->
FriendsFeedEntry(name = name, feedEntry = feedEntry)
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
}
}
@Composable
fun FriendsFeedEntry(
name: String, feedEntry: FeedEntry
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 10.dp)
.weight(11f)
) {
Box(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color(feedEntry.argb_color)),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.weight(10f),
) {
Text(
text = "$name studied for ${feedEntry.subJectName}",
fontWeight = FontWeight.Medium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
Text(
text = feedEntry.taskName,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
Text(
text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString(),
modifier = Modifier
.weight(3f)
.padding(start = 5.dp),
)
}
}
}
}
}

View file

@ -0,0 +1,10 @@
package be.ugent.sel.studeez.screens.friends_feed
import be.ugent.sel.studeez.data.local.models.FeedEntry
sealed interface FriendsFeedUiState {
object Loading : FriendsFeedUiState
data class Succes(
val friendSessions: List<Pair<String, List<Pair<String, FeedEntry>>>>,
) : FriendsFeedUiState
}

View file

@ -0,0 +1,30 @@
package be.ugent.sel.studeez.screens.friends_feed
import androidx.lifecycle.viewModelScope
import be.ugent.sel.studeez.domain.FeedDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class FriendsFeedViewModel @Inject constructor(
feedDAO: FeedDAO,
logService: LogService
) : StudeezViewModel(logService) {
val uiState: StateFlow<FriendsFeedUiState> =
feedDAO.getFriendsSessions()
.map { it.toList() }
.map { FriendsFeedUiState.Succes(it) }
.stateIn(
scope = viewModelScope,
initialValue = FriendsFeedUiState.Loading,
started = SharingStarted.Eagerly,
)
}

View file

@ -1,4 +1,5 @@
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

View file

@ -92,7 +92,8 @@ fun ProfileScreen(
item {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(align = Alignment.CenterHorizontally)
) {
AmountOfFriendsButton(
@ -140,7 +141,7 @@ fun ProfileScreenPreview() {
fun AmountOfFriendsButton(
amountOfFriends: Int,
onClick: () -> Unit
){
) {
Button(
onClick = onClick,
shape = defaultButtonShape()

View file

@ -5,7 +5,6 @@ import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import com.google.firebase.auth.FirebaseAuth
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

View file

@ -82,7 +82,7 @@ fun EditProfileScreen(
)
}
item {
BasicTextButton(
BasicTextButton(
text = AppText.delete_profile,
Modifier.textButton(),
action = editProfileActions.onDeleteClick

View file

@ -1,6 +1,6 @@
package be.ugent.sel.studeez.screens.profile.edit_profile
data class ProfileEditUiState (
data class ProfileEditUiState(
val username: String = "",
val biography: String = ""
)

View file

@ -30,7 +30,7 @@ data class PublicProfileActions(
val getUserDetails: () -> Flow<User>,
val getAmountOfFriends: () -> Flow<Int>,
val onViewFriendsClick: () -> Unit,
val sendFriendRequest: () -> Boolean
val sendFriendRequest: () -> Unit
)
fun getPublicProfileActions(
@ -39,13 +39,17 @@ fun getPublicProfileActions(
): PublicProfileActions {
return PublicProfileActions(
getUserDetails = { viewModel.getUserDetails(viewModel.uiState.value.userId) },
getAmountOfFriends = { viewModel.getAmountOfFriends(
userId = viewModel.uiState.value.userId
) },
getAmountOfFriends = {
viewModel.getAmountOfFriends(
userId = viewModel.uiState.value.userId
)
},
onViewFriendsClick = { viewModel.onViewFriendsClick(open) },
sendFriendRequest = { viewModel.sendFriendRequest(
userId = viewModel.uiState.value.userId
) }
sendFriendRequest = {
viewModel.sendFriendRequest(
userId = viewModel.uiState.value.userId
)
}
)
}
@ -121,15 +125,17 @@ fun PublicProfilePreview() {
PublicProfileScreen(
publicProfileActions = PublicProfileActions(
getUserDetails = {
flowOf(User(
id = "someid",
username = "Maxime De Poorter",
biography = "I am a different student and this is my public profile"
))
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 }
sendFriendRequest = {}
),
popUp = {}
)
@ -138,7 +144,7 @@ fun PublicProfilePreview() {
@Composable
fun PublicProfileEllipsis(
sendFriendRequest: () -> Boolean
sendFriendRequest: () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
@ -147,8 +153,7 @@ fun PublicProfileEllipsis(
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal),
contentDescription = resources().getString(AppText.view_more),
modifier = Modifier.fillMaxSize()
contentDescription = resources().getString(AppText.view_more)
)
}
@ -172,7 +177,7 @@ fun PublicProfileEllipsis(
fun PublicProfileEllipsisPreview() {
StudeezTheme {
PublicProfileEllipsis(
sendFriendRequest = { true }
sendFriendRequest = {}
)
}
}

View file

@ -18,7 +18,7 @@ class PublicProfileViewModel @Inject constructor(
private val friendshipDAO: FriendshipDAO,
selectedUserIdState: SelectedUserId,
logService: LogService
): StudeezViewModel(logService) {
) : StudeezViewModel(logService) {
val uiState = mutableStateOf(
PublicProfileUiState(
@ -53,8 +53,8 @@ class PublicProfileViewModel @Inject constructor(
fun sendFriendRequest(
userId: String
): Boolean {
return friendshipDAO.sendFriendshipRequest(userId)
) {
friendshipDAO.sendFriendshipRequest(userId)
}
}

View file

@ -39,7 +39,8 @@ fun SessionRoute(
val soundPlayer = SoundPlayer(LocalContext.current)
val sessionActions = getSessionActions(viewModel, openAndPopUp)
val sessionScreen = viewModel.getTimer().accept(GetSessionScreenComposable(soundPlayer, open, sessionActions))
val sessionScreen =
viewModel.getTimer().accept(GetSessionScreenComposable(soundPlayer, open, sessionActions))
sessionScreen()
}

View file

@ -21,7 +21,7 @@ class SoundPlayer(private val context: Context) {
private fun initPlayer(): MediaPlayer {
return MediaPlayer.create(
return MediaPlayer.create(
context,
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
)

View file

@ -27,7 +27,7 @@ fun BreakSessionScreenComposable(
sessionActions = sessionActions,
midSection = { Dots(pomodoroTimer = pomodoroTimer) },
callMediaPlayer = { soundPlayer.playOn(pomodoroTimer.hasCurrentCountdownEnded()) },
motivationString = { motivationString (pomodoroTimer = pomodoroTimer) }
motivationString = { motivationString(pomodoroTimer = pomodoroTimer) }
)
}
@ -57,11 +57,13 @@ private fun Dots(pomodoroTimer: FunctionalPomodoroTimer): Int {
@Composable
private fun Dot(color: Color) {
Box(modifier = Modifier
.padding(5.dp)
.size(10.dp)
.clip(CircleShape)
.background(color))
Box(
modifier = Modifier
.padding(5.dp)
.size(10.dp)
.clip(CircleShape)
.background(color)
)
}

View file

@ -12,11 +12,12 @@ class GetSessionScreenComposable(
private val soundPlayer: SoundPlayer,
private val open: (String) -> Unit,
private val sessionActions: SessionActions
) :
) :
FunctionalTimerVisitor<@Composable () -> Unit> {
override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): @Composable () -> Unit {
return { CustomTimerSessionScreenComposable(
return {
CustomTimerSessionScreenComposable(
open = open,
sessionActions = sessionActions,
soundPlayer = soundPlayer,

View file

@ -23,10 +23,10 @@ fun SessionScreen(
open: (String) -> Unit,
sessionActions: SessionActions,
callMediaPlayer: () -> Unit = {},
midSection: @Composable () -> Int = {0},
midSection: @Composable () -> Int = { 0 },
motivationString: @Composable () -> String,
) {
) {
Column(
modifier = Modifier.padding(10.dp)
) {

View file

@ -135,9 +135,11 @@ fun SessionRecapScreenPreview() {
SessionRecapScreen(
modifier = Modifier,
sessionRecapActions = SessionRecapActions(
{ SessionReport(
studyTime = 100,
) },
{
SessionReport(
studyTime = 100,
)
},
{},
{},
)

View file

@ -1,42 +0,0 @@
package be.ugent.sel.studeez.screens.sessions
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun SessionsRoute(
// viewModel: SessionsViewModel,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions
) {
SessionsScreen(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
)
}
@Composable
fun SessionsScreen(
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions
) {
PrimaryScreenTemplate(
title = resources().getString(AppText.upcoming_sessions),
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
) {
Text(
text = resources().getString(AppText.sessions_temp_description),
modifier = Modifier.fillMaxSize(),
textAlign = TextAlign.Center
)
}
}

View file

@ -21,7 +21,7 @@ class SignUpViewModel @Inject constructor(
private val accountDAO: AccountDAO,
private val userDAO: UserDAO,
logService: LogService
) : StudeezViewModel(logService) {
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(SignUpUiState())
private set
@ -35,6 +35,7 @@ class SignUpViewModel @Inject constructor(
fun onUsernameChange(newValue: String) {
uiState.value = uiState.value.copy(username = newValue)
}
fun onEmailChange(newValue: String) {
uiState.value = uiState.value.copy(email = newValue)
}

View file

@ -26,7 +26,7 @@ class SplashViewModel @Inject constructor(
showError.value = false
if (accountDAO.hasUser) {
openAndPopUp(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SPLASH_SCREEN)
} else{
} else {
openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.SPLASH_SCREEN)
}
}

View file

@ -9,7 +9,7 @@ import be.ugent.sel.studeez.screens.timer_form.form_screens.BreakTimerFormScreen
import be.ugent.sel.studeez.screens.timer_form.form_screens.CustomTimerFormScreen
import be.ugent.sel.studeez.screens.timer_form.form_screens.EndlessTimerFormScreen
class GetTimerFormScreen: TimerInfoVisitor<AbstractTimerFormScreen> {
class GetTimerFormScreen : TimerInfoVisitor<AbstractTimerFormScreen> {
override fun visitCustomTimerInfo(customTimerInfo: CustomTimerInfo): AbstractTimerFormScreen {
return CustomTimerFormScreen(customTimerInfo)

View file

@ -18,10 +18,10 @@ fun TimerAddRoute(
TimerFormScreen(
popUp = popUp,
getTimerInfo = viewModel::getTimerInfo,
extraButton= { },
extraButton = { },
AppText.add_timer
) {
viewModel.saveTimer(it, goBack = {popUp(); popUp()})
viewModel.saveTimer(it, goBack = { popUp(); popUp() })
}
}
@ -42,7 +42,7 @@ fun TimerEditRoute(
TimerFormScreen(
popUp = popUp,
getTimerInfo = viewModel::getTimerInfo,
extraButton= { deleteButton() },
extraButton = { deleteButton() },
AppText.edit_timer
) {
viewModel.editTimer(it, goBack = popUp)

View file

@ -1,7 +1,8 @@
package be.ugent.sel.studeez.screens.timer_form.form_screens
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import be.ugent.sel.studeez.R

View file

@ -1,6 +1,8 @@
package be.ugent.sel.studeez.screens.timer_form.form_screens
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import be.ugent.sel.studeez.R
@ -13,8 +15,7 @@ import be.ugent.sel.studeez.R.string as AppText
class BreakTimerFormScreen(
private val breakTimerInfo: PomodoroTimerInfo
): AbstractTimerFormScreen(breakTimerInfo) {
) : AbstractTimerFormScreen(breakTimerInfo) {
@Composable
@ -28,7 +29,7 @@ class BreakTimerFormScreen(
breakTimerInfo.breakTime = newTime
}
valids["repeats"] = remember {mutableStateOf(true)}
valids["repeats"] = remember { mutableStateOf(true) }
firsts["repeats"] = remember { mutableStateOf(true) }
LabeledErrorTextField(

View file

@ -9,7 +9,7 @@ import be.ugent.sel.studeez.R.string as AppText
class CustomTimerFormScreen(
private val customTimerInfo: CustomTimerInfo
): AbstractTimerFormScreen(customTimerInfo) {
) : AbstractTimerFormScreen(customTimerInfo) {
@Composable
override fun ExtraFields() {

View file

@ -7,7 +7,7 @@ import be.ugent.sel.studeez.ui.theme.StudeezTheme
class EndlessTimerFormScreen(
endlessTimerInfo: EndlessTimerInfo
): AbstractTimerFormScreen(endlessTimerInfo) {
) : AbstractTimerFormScreen(endlessTimerInfo) {
}
@Preview

View file

@ -1,6 +1,8 @@
package be.ugent.sel.studeez.screens.timer_form.timer_type_select
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -12,10 +14,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
import be.ugent.sel.studeez.data.local.models.timer_info.*
import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.*
import be.ugent.sel.studeez.R.string as AppText
import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.CUSTOM
import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.BREAK
import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.ENDLESS
val defaultTimerInfo: Map<TimerType, TimerInfo> = mapOf(
CUSTOM to CustomTimerInfo("", "", 0),
@ -40,7 +40,9 @@ fun TimerTypeSelectScreen(
val default: TimerInfo = defaultTimerInfo.getValue(timerType)
Button(
onClick = { viewModel.onTimerTypeChosen(default, open) },
modifier = Modifier.fillMaxWidth().padding(5.dp)
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
) {
Text(text = timerType.name)
}

View file

@ -68,11 +68,13 @@ fun TimerOverviewScreen(
LazyColumn {
// Custom timer, select new duration each time
item {
TimerEntry(timerInfo = CustomTimerInfo(
name = resources().getString(R.string.custom_name),
description = resources().getString(R.string.custom_name),
studyTime = 0
))
TimerEntry(
timerInfo = CustomTimerInfo(
name = resources().getString(R.string.custom_name),
description = resources().getString(R.string.custom_name),
studyTime = 0
)
)
}
// Default Timers, cannot be edited
items(timerOverviewActions.getDefaultTimers()) {

View file

@ -2,6 +2,6 @@ package be.ugent.sel.studeez.ui.theme
import androidx.compose.ui.graphics.Color
val Blue100 = Color( 30, 100, 200, 255)
val Blue120 = Color( 27, 90, 180, 255)
val Yellow100 = Color(255, 210, 0, 255)
val Blue100 = Color(30, 100, 200, 255)
val Blue120 = Color(27, 90, 180, 255)
val Yellow100 = Color(255, 210, 0, 255)

View file

@ -36,6 +36,9 @@
<string name="your_feed">This is your feed</string>
<string name="empty_feed_help_text">Click here to create you first subject and tasks to get started</string>
<!-- Friends Feed -->
<string name="friends_feed">Feed</string>
<!-- Tasks -->
<string name="tasks">Tasks</string>
<string name="task">Task</string>
@ -134,6 +137,9 @@
<string name="send_friend_request">Send friend request</string>
<string name="remove_friend">Remove as friend</string>
<string name="show_profile">Show profile</string>
<string name="click_search_friends">Click to search friends</string>
<string name="searching_friends">Searching friends</string>
<string name="already_friend">You are already befriended with that person.</string>
<!-- ========== Create & edit screens ========== -->