Merge pull request #125 from SELab1/feed_friends

Feed friends
This commit is contained in:
brreynie 2023-05-16 11:11:10 +02:00 committed by GitHub Enterprise
commit 373a0e6209
16 changed files with 271 additions and 96 deletions

View file

@ -8,13 +8,15 @@ 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.Check
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 +101,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,6 +2,7 @@ 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
@ -27,7 +28,7 @@ class NavigationBarViewModel @Inject constructor(
}
fun onSessionsClick(open: (String) -> Unit) {
open(SESSIONS_SCREEN)
open(FRIENDS_FEED)
}
fun onProfileClick(open: (String) -> Unit) {

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,9 @@
package be.ugent.sel.studeez.domain
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.User
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import kotlinx.coroutines.flow.Flow
@ -10,11 +12,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

@ -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,10 @@ 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 +121,17 @@ 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,15 +1,14 @@
package be.ugent.sel.studeez.domain.implementation
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.User
import be.ugent.sel.studeez.data.local.models.task.Task
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.*
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
@ -17,6 +16,7 @@ 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 com.google.firebase.firestore.ktx.toObject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
@ -26,8 +26,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 +35,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,14 @@ 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,9 @@ 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

@ -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,6 +16,8 @@ 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.friends_feed.FriendsFeedScreen
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
@ -23,7 +25,6 @@ import be.ugent.sel.studeez.screens.profile.ProfileRoute
import be.ugent.sel.studeez.screens.profile.public_profile.PublicProfileRoute
import be.ugent.sel.studeez.screens.session.SessionRoute
import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute
import be.ugent.sel.studeez.screens.sessions.SessionsRoute
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
@ -135,10 +136,11 @@ fun StudeezNavGraph(
}
composable(StudeezDestinations.SESSIONS_SCREEN) {
SessionsRoute(
composable(StudeezDestinations.FRIENDS_FEED) {
FriendsFeedRoute(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
navigationBarActions = navigationBarActions,
viewModel = hiltViewModel()
)
}

View file

@ -0,0 +1,134 @@
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.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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 androidx.compose.ui.unit.sp
import be.ugent.sel.studeez.common.composable.DateText
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.StealthButton
import be.ugent.sel.studeez.common.composable.TimerEntry
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.ext.spacer
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.Task
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import be.ugent.sel.studeez.resources
import kotlinx.coroutines.flow.toList
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun FriendsFeedRoute(
viewModel: FriendsFeedViewModel,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions
) {
FriendsFeedScreen(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
viewModel = viewModel
)
}
@Composable
fun FriendsFeedScreen(
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
viewModel: FriendsFeedViewModel
) {
PrimaryScreenTemplate(
title = resources().getString(AppText.friends_feed),
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
) {
val friendsSessions = viewModel.getFriendsSessions().collectAsState(initial = emptyList())
LazyColumn() {
// Default Timers, cannot be edited
items(friendsSessions.value) {
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)
) {
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())
}
}
}
}
}

View file

@ -0,0 +1,27 @@
package be.ugent.sel.studeez.screens.friends_feed
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.domain.FeedDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.SessionDAO
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import javax.inject.Inject
@HiltViewModel
class FriendsFeedViewModel @Inject constructor(
private val feedDAO: FeedDAO,
logService: LogService
) : StudeezViewModel(logService) {
fun getFriendsSessions(): Flow<List<Pair<String, List<Pair<String, FeedEntry>>>>> {
return feedDAO.getFriendsSessions().map { it.toList() }
}
}

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

@ -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>